I have a treeView element where every node represent a double list.
I'm using a DataVisualization.Charting control to display the values in the list.
For some of the lists I get an exception after RecalculateAxesScale (System.OverflowException: Value was either too large or too small for a Decimal).
I ignore this error and therefore the chart displays a big red cross.
When I now click on another node I want to display the chart of this double list (which is valid), but my chart is not redrawn. It always displays the red X.
My code:
private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
{
//Refresh chart:
chart1.Series.Clear();
chart1.ResetAutoValues();
chart1.ResetText();
//plot new doublelist
var series = new Series
{
Name = "name",
Color = color,
ChartType = SeriesChartType.Line,
ChartArea = "chartName"
};
this.chart1.Series.Add(series);
series.Points.DataBindY(doubleList);
var chartArea = chart1.ChartAreas["chartName"];
chartArea.RecalculateAxesScale();
chartArea.AxisX.Minimum = 1;
chartArea.AxisX.Maximum = doubleList.Count;
chartArea.CursorX.AutoScroll = true;
chartArea.CursorY.AutoScroll = true;
// Allow user to select area for zooming
chartArea.CursorX.IsUserEnabled = true;
chartArea.CursorX.IsUserSelectionEnabled = true;
// Set automatic zooming`<br>
chartArea.AxisX.ScaleView.Zoomable = true;
chartArea.AxisY.ScaleView.Zoomable = true;
chartArea.AxisX.ScrollBar.IsPositionedInside = true;
//reset zoom
chartArea.AxisX.ScaleView.ZoomReset();
chart1.Invalidate();
}
[EDIT]
Type of dblList:
List<double> doubleList= (from s in myData select s.value).ToList();
Full stack of Exceptions:
{System.OverflowException: Value was either too large or too small for a Decimal.
at System.Decimal.FCallMultiply(Decimal& d1, Decimal& d2)
at System.Decimal.op_Multiply(Decimal d1, Decimal d2)
at System.Windows.Forms.DataVisualization.Charting.Axis.RoundedValues(Double inter, Boolean shouldStartFromZero, Boolean autoMax, Boolean autoMin, Double& min, Double& max)
at System.Windows.Forms.DataVisualization.Charting.Axis.EstimateNumberAxis(Double& minimumValue, Double& maximumValue, Boolean shouldStartFromZero, Int32 preferredNumberOfIntervals, Boolean autoMaximum, Boolean autoMinimum)
at System.Windows.Forms.DataVisualization.Charting.Axis.EstimateAxis(Double& minimumValue, Double& maximumValue, Boolean autoMaximum, Boolean autoMinimum)
at System.Windows.Forms.DataVisualization.Charting.Axis.EstimateAxis()
at System.Windows.Forms.DataVisualization.Charting.ChartArea.SetDefaultAxesValues()
at System.Windows.Forms.DataVisualization.Charting.ChartArea.SetData(Boolean initializeAxes, Boolean checkIndexedAligned)
at System.Windows.Forms.DataVisualization.Charting.ChartArea.RecalculateAxesScale()
[EDIT 2]
Example List:
List<double> dblList = new List<double>();
dblList.Add(0.0);
dblList.Add(-7.4876421623346545E-36);
dblList.Add(1.0);
dblList.Add(-26697097281536.0);
dblList.Add(-6.8163553952838136E+28); //problem!!!!!
The last value produces the problem (red cross without exception). So it seems that the min and max values for converting the list are not appropriate. Any ideas on that?
double min = (double)Decimal.MinValue; //min = -7.9228162514264338E+28
double max = (double)Decimal.MaxValue; //max = 7.9228162514264338E+28
The error in RecalculateAxesScale (System.OverflowException: Value was either too large or too small for a Decimal) was thrown, because of a Bug in the Library. It uses decimal values inside the implementation of chart functions (especially in axes recalculation/zoom), which causes the problem.
Just checking for every chart point if its value is lower or greater than
double min = (double)Decimal.MinValue;
double max = (double)Decimal.MaxValue;
and replace it by this values didn't solve the problem.
I chose -/+7.92E+27 as bounds instead of the ones above and everything is working fine now.
So the solution proffered thus far is almost, but not quite, correct. If it was open source I could find an remedy the problem directly, but alas I must only surmise as to the actual implementation. The chart library appears to convert to Decimal, and throws an exception when the value range is exceeded while converting from double. The obvious solution of checking for all out-of-range values and setting to Decimal.MinValue and Decimal.MaxValue fails with the same exception. In fact, the actual max and min values allowed by the charting engine was found by myself, through some experimentation, to be approximately an order of magnitude less.
Enough discussion, here is my working source code for a TVQ chart with sometimes bound-exceeding double values. Max and min chart values are pre-calculated for efficiency. Replace all calls to AddXY with SafeChartDouble. Change the AxisX datatype as necessary for your application.
private static readonly double SCALE_FACTOR = 10;
private static readonly double MIN_CHART_VALUE
= Convert.ToDouble(Decimal.MinValue) / SCALE_FACTOR;
private static readonly double MAX_CHART_VALUE
= Convert.ToDouble(Decimal.MaxValue) / SCALE_FACTOR;
private void SafeChartDouble(Series cs, DateTime ts, double dv)
{
// microsoft chart component breaks on very large/small values
double chartv;
if (dv < MIN_CHART_VALUE)
{
chartv = MIN_CHART_VALUE;
}
else if (dv > MAX_CHART_VALUE)
{
chartv = MAX_CHART_VALUE;
}
else
{
chartv = dv;
}
cs.Points.AddXY(ts, chartv);
}
Related
I'm plotting a few different LineSeries in Oxyplot (in C#, using Windows Forms), which all have wildly different ranges. To make every series still visible, I'm scaling all their values to the range from 0 to 1. Of course the problem here is, that the actual values of the series can't be displayed anymore, so I wondered if it's possible to change the left-click event that displays the X and Y value of a DataPoint to fix this. If possible, I'd like it so that when the user clicks on a datapoint, the displayed Y-value would be scaled back to the original, while the graph remains scaled down.
For example, I have a Na+ value ranging from 130 to 150, which I then scale to 0 and 1. But when the user clicks on a Datapoint, I want it to display Y = 140 and not 0.5. Since every LineSeries has a different scaling factor, this would also mean that I'd have to edit the label for each series seperately.
So yeah, is something like that possible in the current version? Thanks in advance!
Edit: I figured it out, see my answer below!
I'm not sure if I'm supposed to answer my own question, but I ended up figuring it out. So anyway, it turns out I can't access any of the properties needed, so I just made my own class for the Datapoints and included a property for the scaled value. It turns out that the Tracker is capable of displaying other properties of the Datapoints, but the standard Datapoints only has X and Y. Then I modified the TrackerFormatString to show the scaled value instead of the actual one.
public class MeasurePoint : IDataPointProvider
{
public MeasurePoint(double x, double y, double scale)
{
X = x; Y = y; Scale = scale;
}
public double X { get; set; }
public double Y { get; set; }
public double Scale { get; set; }
public DataPoint GetDataPoint()
{
return new DataPoint(X, Y);
}
}
That is the class I created and this is how I ended up handling the points.
var points = new MeasurePoint[Y.Count - 1];
for (int i = 0; i < Y.Count; i++)
{
points[i] = new MeasurePoint(DateTimeAxis.ToDouble(X[i]), Y[i], Y[i]*scale);
}
Series.ItemsSource = points;
Series.TrackerFormatString = "{0}\n{2}\n{Scale}";
scale here is the factor that I divide the values with before plotting and the TrackerFormatString is {0} the Series name and {2} is the X value.
Works pretty great!
You can't directly change the displayed data, however you can listen to the mousedown event within the oxyplot like this:
var model= new PlotModel();
model.MouseDown += Model_MouseDown;
private void Model_MouseDown(object sender, OxyMouseDownEventArgs e)
{
var controller = sender as PlotController;
var position = e.HitTestResult;
}
With the values of position you can calculate back to the actual value and display it somewhere else.
So, here's the problem.
I have a chart that displays two columns, Completed and Uncompleted, across a number of work types, using the following loop:
foreach (var workType in model.WorkTypes)
{
decimal completed = 0;
decimal uncompleted = 0;
decimal workSubmitted = 0;
decimal completionRate= 0;
foreach (var rec in model.JobList.Where(x => x.jobType== workType.Id))
{
uncompleted += model.JobList.SingleOrDefault(x => x.recID== rec.recID && x.jobType == workType.Id).Uncompleted;
completed += model.JobList.SingleOrDefault(x => x.recID == rec.recID && x.jobType == workType.Id).Completed;
}
workSubmitted = uncompleted + completed;
if (uncompleted != 0)
{
completionRate= (completed/ workSubmitted) * 100;
}
myChart.Series["Uncompleted"].Points.AddXY(workType.TypeName, uncompleted );
myChart.Series["Completed"].Points.AddXY(workType.TypeName, completed);
}
What I am trying to do is have it display a label above the two columns that displays the completionRate value as a percentage for each workType.
Any help or advice would be appreciated.
This is the current look of the chart:
By default the Labels show the y-value but you can set an arbitrary Label for each DataPoint e.g. when you add the point like this:
int p = myChart.Series["Uncompleted"].Points.AddXY(workType.TypeName, rejections);
myChart.Series["Uncompleted"].Points[p].Label = sometext;
And of course you can calculate the text for the label as needed, e.g.:
string sometext = (workSubmitted / rejections * 100).ToString("0.00") + "%";
Note that you must update the Label after changing the values in your calculation. No automatic expressions are supported!
Update
As I wrote, placing a Label centered at the x-value the columns share, is hard or even impossible; that is because a Label belongs to an individual data point. This is a unique problem with column (and bar) type charts, since here the points of the series are displayed in clusters around the common x-value. (We could workaround if and only if we had an odd number of series by adding the labels to the mid points)
So we need to use Annotations. Here is a function that will place a TextAnnotation centered at the x-value and at the height of the larger y-value of two data points..:
void setCenterAnnotation(Chart chart, ChartArea ca,
DataPoint dp1, DataPoint dp2, string lbl)
{
TextAnnotation ta = new TextAnnotation();
ta.Alignment = ContentAlignment.BottomCenter;
ta.AnchorAlignment = ContentAlignment.TopCenter;
DataPoint dp = dp1.YValues[0] > dp2.YValues[0] ? dp1 : dp2;
ta.Height = 0.36f;
ta.AxisX = ca.AxisX;
ta.AxisY = ca.AxisY;
ta.AnchorDataPoint = dp;
ta.AnchorX = dp1.XValue;
ta.Text = lbl;
chart.Annotations.Add(ta);
}
If you have more than two Series you would best determine the anchorpoint, i.e. the one with the larger value before, and pass it instead of the two points I pass here..
Placing/anchoring annotations is not really obvious, so here are a few notes:
I anchor to a DataPoint to make it show at the height of its y-value.
To use (axis-)values for anchoring one has to assign one or both axes to it.
I then (order matters!) set the AnchorX property so that it is not centered over a point but over the common x-value.
I also set some Height or else the text won't move up on top of the column; not quite sure what the rationale is here..
Here is the result:
I had added the anotations while adding the points:
int ix = s1.Points.AddXY(i, rnd.Next(i+7));
s2.Points.AddXY(i, rnd.Next(i+4)+3);
double vmax = Math.Max(s1.Points[ix].YValues[0], s2.Points[ix].YValues[0]);
string lbl = (vmax / 123f).ToString("0.0") + "%";
setCenterAnnotation(chart12, ca, s1.Points[ix], s2.Points[ix], lbl );
I need to represent a percent of value as a graph in DevExpress grid cell. I am able to paint using DrawLine but my problem is as soon as the percent value is equal to greater than 1 it is treated as 100% in this code. Please find the code below, As shown in the screenshot, 3.59 should be shown less than 8.35! Please help.
private void CustomDrawCell(object sender, RowCellCustomDrawEventArgs args)
{
args.Appearance.DrawBackground(args.Graphics, args.Cache, args.Bounds);
if (column != null)
{
int penSize = args.Bounds.Height * 2 / 3;
double value = GetValue(); // This is the value against which I have to display the graph, its in %.
int left = args.Bounds.Left;
int middle = args.Bounds.Height / 2 + args.Bounds.Top;
int width = args.Bounds.Width;
int right = (int)(left + width * value);
args.Graphics.DrawLine(new Pen(Color.Green, penSize), left, middle, right, middle);
}
args.Handled = true;
}
int right = (int)(left + width * value);
This code calculates the bar length correctly only if the value is between 0 and 1. If values are between 0 and 100 or between 0 and 10, you need to divide the result of multiplying by 100 or 10 correspondingly.
int right = (int)(left + width * value / 100);
By the way, it is not necessary to puzzle over custom drawing, because you are using XtraGrid. There is the RepositoryItemProgressBar component, which can be embedded into XtraGrid cell. It displays the line according to the cell value and allows you to define the maximum and minimum value, so that the line is most exactly visualize the cell value. Read this article to learn how to assign editors to columns: Assigning Editors to Columns and Card Fields
I'm trying to draw a signal response on a chart and I need a logarithmic scale for X and Y.
I defined two functions, one for X axis
private void Configure_Axis_X(bool Logaritmic, double Maximum, double Minimum, double Interval)
{
CH_EQ_Chart.ChartAreas[0].AxisX.IsLogarithmic = Logaritmic;
CH_EQ_Chart.ChartAreas[0].AxisX.Minimum = Minimum;
CH_EQ_Chart.ChartAreas[0].AxisX.Maximum = Maximum;
CH_EQ_Chart.ChartAreas[0].AxisX.Interval = Interval;
CH_EQ_Chart.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.Black;
CH_EQ_Chart.ChartAreas[0].AxisX.MajorGrid.LineDashStyle = Graph.ChartDashStyle.Dash;
}
and one for Y axis
private void Configure_Axis_Y(bool Logaritmic, double Maximum, double Minimum, double Interval)
{
CH_EQ_Chart.ChartAreas[0].AxisY.IsLogarithmic = Logaritmic;
CH_EQ_Chart.ChartAreas[0].AxisY.Minimum = Minimum;
CH_EQ_Chart.ChartAreas[0].AxisY.Maximum = Maximum;
CH_EQ_Chart.ChartAreas[0].AxisY.Interval = Interval;
CH_EQ_Chart.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.Black;
CH_EQ_Chart.ChartAreas[0].AxisY.MajorGrid.LineDashStyle = Graph.ChartDashStyle.Dash;
}
the response to draw is expressed in Decibel and I'd like to have logarithmic scale also for Y.
When I have the array with values, I get minimum and maxim value and I try to use the function above with
double Abs_Max = Math.Max(y.Max(), z.Max());
double Abs_Min = Math.Min(y.Min(), z.Min());
Configure_Axis_Y(true, Abs_Max + Abs_Max/10, Abs_Min + Abs_Min/10, 20);
but when I select islogaritmic = true a red cross appears instead the graph.
If I set islogaritmic = false the picture appears right.
The red cross is the chart's way of handling an exception during drawing. The most likely culprit when dealing with log scale is that one or more data points have zero or negative values. Check in the debugger what Abs_Min is when the method is called, as it's likely you're somehow getting zero or negative values in there.
Good afternoon,
Wow what a title.
Basically here is the thing.
If have a time series which is not continuous.
Hence, since I use the IsXValueIndexed property of the Series (set to true) to collapse the space between the separate points.
That works fine, however I would now like to be able to recover a point's detail in from the graph (X and Y values) and display them in a label on the form.
Hence, I use the following event:
void myChart_CursorPositionChanging(object sender, CursorEventArgs e)
{
if (!double.IsNaN(e.NewPosition))
{
if (e.Axis.AxisName == AxisName.X)
{
lbl_selDate.Content = DateTime.FromOADate(e.NewPosition);
}
else
{
lbl_selValue.Content = e.NewPosition;
}
}
}
The problem is that the date is incorrect... I cannot find the right conversion method to recover this damned timestamp.
Can you by any chance help me out?
Thanks!
Jeremie
suppose you have x-axis of type DateTime then use:
DateTime xValue = DateTime.FromOADate(((Chart)sender).Series[0].Points[(int)e.NewPosition - 1].XValue)
suppose you have y-axis of type double then use:
double yValue = ((Chart)sender).Series[0].Points[(int)e.NewPosition - 1].YValues[0];