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.
Related
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 );
is there a way to get the rectangles of the stackcolumn chart bar?
this code snippet is how it can be works but it's very ugly:
var points = new List<Point>();
for (int x = 0; x < chart.Size.Width; x++)
{
for (int y = 0; y < chart.Size.Height; y++)
{
var hp = chart.HitTest(x, y, false, ChartElementType.DataPoint);
var result = hp.Where(h => h.Series?.Name == "Cats");
if (result.Count() > 0)
{
points.Add(new Point(x, y));
}
}
}
var bottomright = points.First();
var topleft = points.Last();
I will try to describe my purpose:
I would like to create a chart from various testresults and make this available as a HTML file. This generated Chart is inserted as an image file in the HTML document. Now, I would like to link each part of a Bar area from the Chart to an external document. Since the graphics is static, I have only the possibility to use the "MAP Area" element to make any area as a link from HTML. The "map" element requires a "rectangle", or these coordinates. That's the reason why I need the coordinator of each part of a Bar.
I have to mention that I am not really familiar with the Chart control yet.
The graphics is generated testweise.
[SOLVED]
i got the solution:
var stackedColumns = new List<Tuple<string,string,Rectangle>>();
for (int p = 0; p < chart.Series.Select(sm => sm.Points.Count).Max(); p++)
{
var totalPoints = 0;
foreach (var series in chart.Series)
{
var width = int.Parse(series.GetCustomProperty("PixelPointWidth"));
var x = (int)area.AxisX.ValueToPixelPosition(p + 1) - (width / 2);
int y = (int)area.AxisY.ValueToPixelPosition(totalPoints);
totalPoints += series.Points.Count > p ? (int)series.Points[p].YValues[0] : 0;
int y_total = (int)area.AxisY.ValueToPixelPosition(totalPoints);
var rect = new Rectangle(x, y_total, width, Math.Abs(y - y_total));
stackedColumns.Add(new Tuple<string, string, Rectangle>(series.Name, series.Points.ElementAtOrDefault(p)?.AxisLabel, rect));
}
}
this workaround works for stackedcolumn and points starts at x-axis=0.
just the PixelPointWidth property has to be set manualy to get the right width. i have not yet found a way to get the default bar width..
This is extremely tricky and I really wish I knew how to get the bounds from some chart functionionality!
You code snippet is actulally a good start for a workaround. I agree though that it has issues:
It is ugly
It doesn't always work
It has terrible performance
Let's tackle these issues one by one:
Yes it is ugly, but then that's the way of workarounds. My solution is even uglier ;-)
There are two things I found don't work:
You can't call a HitTest during a Pre/PostPaint event or terrible things will happen, like some Series go missing, SO exceptions or other crashes..
The result for the widths of the last Series are off by 1-2 pixels.
The performance of testing each pixel in the chart will be terrible even for small charts, but gets worse and worse when you enlarge the chart. This is relatively easy to prevent, though..:
What we are searching are bounding rectangles for each DataPoint of each Series.
A rectangle is defined by left and right or width plus top and bottom or height.
We can get precise values for top and bottom by using the axis function ValueToPixelPosition feeding in the y-value and 0 for each point. This is simple and cheap.
With that out of the way we still need to find the left and right edges of the points. To do so all we need to do it test along the zero-line. (All points will either start or end there!)
This greatly reduces the number of tests.
I have decided to do the testing for each series separately, restaring at 0 each time. For even better performance one could do it all in one go.
Here is a function that returns a List<Rectangle> for a given Series:
List<Rectangle> GetColumnSeriesRectangles(Series s, Chart chart, ChartArea ca)
{
ca.RecalculateAxesScale();
List<Rectangle> rex = new List<Rectangle>();
int loff = s == chart.Series.Last() ? 2 : 0; ;
int y0 = (int)ca.AxisY.ValueToPixelPosition(0);
int left = -1;
int right = -1;
foreach (var dp in s.Points)
{
left = -1;
int delta = 0;
int off = dp.YValues[0] > 0 ? delta : -delta;
for (int x = 0; x < chart.Width; x++)
{
var hitt = chart.HitTest(x, y0 +off );
if (hitt.ChartElementType == ChartElementType.DataPoint &&
((DataPoint)hitt.Object) == dp)
{
if (left < 0) left = x;
right = x;
}
else if (left > 0 && right > left) break;
}
int y = (int)ca.AxisY.ValueToPixelPosition(dp.YValues[0]);
rex.Add(new Rectangle(left, Math.Min(y0, y),
right - left + 1 - loff, Math.Abs(y - y0)));
left = -1;
}
return rex;
}
A few notes:
I start by doing a RecalculateAxesScale because we can't Hittest before the current layout has been calculated.
I use a helper variable loff to hold the offset for the width in the last Series.
I start searching at the last x coordinate as the points should all lie in sequence. If they don't because you have used funny x-values or inserted points you may need to start at 0 instead..
I use y0 as the baseline of the zero values for both the hittesting y and also the points' base.
I use a little Math to get the bounds right for both positive and negative y-values.
Here is a structure to hold those rectangles for all Series and code to collect them:
Dictionary<string, List<Rectangle>> ChartColumnRectangles = null;
Dictionary<string, List<Rectangle>> GetChartColumnRectangles(Chart chart, ChartArea ca)
{
Dictionary<string, List<Rectangle>> allrex = new Dictionary<string, List<Rectangle>>();
foreach (var s in chart.Series)
{
allrex.Add(s.Name, GetColumnSeriesRectangles(s, chart, ca));
}
return allrex;
}
We need to re-calculate the rectangles whenever we add points or resize the chart; also whenever the axis view changes. The common code for AxisViewChanged, ClientSizeChanged, Resize and any spot you add or remove points could look like this:
Chart chart= sender as Chart;
GetChartColumnRectangles(chart, chart.ChartAreas[0]);
Let's test the result with a Paint event:
private void chart1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
chart1.ApplyPaletteColors();
foreach (var kv in ChartColumnRectangles)
{
{
foreach (var r in kv.Value)
g.DrawRectangle(Pens.Black, r);
}
}
}
Here it is in action:
Well, I've been down this path and the BIG issue for me is that the custom property of 'PixelPointWidth' is just that - it is custom. You cannot retrieve it unless you've set it. I needed the width of the item - had to scwag/calculate it myself. Keep in mind that many charts can be panned/zoomed, so once you go down this path, then you need to recalculate it and set it for the chart prepaint events.
Here is a crude little function I made (is more verbose than needed - for educational purposes and has no error handling :)):
private int CalculateChartPixelPointWidth(Chart chart, ChartArea chartArea, Series series)
{
// Get right side - takes some goofy stuff - as the pixel location isn't available
var areaRightX = Math.Round(GetChartAreaRightPositionX(chart, chartArea));
var xValue = series.Points[0].XValue;
var xPixelValue = chartArea.AxisX.ValueToPixelPosition(xValue);
var seriesLeftX = chart.Location.X + xPixelValue;
var viewPointWidth = Math.Round((areaRightX - seriesLeftX - (series.Points.Count * 2)) / series.Points.Count, 0);
return Convert.ToInt32(viewPointWidth);
}
And this as well:
private double GetChartAreaRightPositionX(Chart chart, ChartArea area)
{
var xLoc = chart.Location.X;
return xLoc + (area.Position.Width + area.Position.X) / 100 * chart.Size.Width;
}
The reason I'm calculating this is because I need to draw some graphical overlays on top of the normal chart item objects (my own rendering for my own purposes).
In the 'prepaint' event for the chart, I need to calculate the 'PixelPointWidth' that matches the current chart view (might be panned/zoomed). I then use that value to SET the chart custom property to match . . . such that the normal chart entities and MINE are correctly aligned/scaled (ensures we're in exactly the right 'x' axis position):
In my prepaint event, I do the following - just prior to drawing my graphical entities:
// Pretty close scwag . . .
var viewPointWidth = CalculateChartPixelPointWidth(e.Chart, e.Chart.ChartAreas[0], e.Chart.Series[0]);
// Set the custom property and use the same point width for my own entities . .
chart1.Series[0].SetCustomProperty("PixelPointWidth", viewPointWidth.ToString("D"));
// . . . now draw my entities below . . .
I use the C# Chart in WinForms to plot a variety of variables in real time using the "line" chart type. That works well for analog values, but it's less than ideal for on/off flags.
I'd like to plot multiple flags as horizontal bars that are filled when the value is '1" and clear when the value is '0'.
Before I start coding a solution from scratch, do you have any suggestion on how I could take advantage of any features of the "chart" object to implement this more effectively?
EDIT: I am playing with the Area type, and it seems to be promising.
EDIT 2: That didn't work, because the area in the Area type always starts at the bottom of the chart, hiding the other rows. I am now trying the Range Column type
There are several ways to tackle this.: StackedBars, AreaChart, Annotations but I think by far the simplest is using a LineChartType.
The first issue is: How to create the gaps? The simplest way is to draw them as lines but with Color.Transparent. So instead of using the flag value as our y-value we use it to set the color..
So we could use a function like this:
void AddFlagLine(Chart chart, int series, int flag, int x)
{
Series s = chart.Series[series];
int px = s.Points.AddXY(x, series);
s.Points[px].Color = s.Color;
if (px > 0) s.Points[px - 1].Color = flag == 1 ? s.Color : Color.Transparent;
}
It takes the index of your Series and uses the flag to determine the color; note that the color of a line segment is controlled by the color of the end point.
So if you want to have the line going out from the new point to have its flag color, you need to set it when adding the next one..
This is simple enough and for lines as thick as 1-10 it works fine. But if you want larger widths things get a bit ugly..:
The rounded caps start to get bigger and bigger until they actually touch, flling the gaps more or less.
Unfortunately there seems to be no way to controls the caps-style of the lines. There are many CustomAttributes including DashStyles but not this one. So we have to resort to owner-drawing. This is rather simple for line charts. Here is an example:
The xxxPaint event looks like this:
private void chart_PostPaint(object sender, ChartPaintEventArgs e)
{
Graphics g = e.ChartGraphics.Graphics;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
for (int si = 0; si < chart.Series.Count; si++ )
{
Series s = chart.Series[si];
for (int pi = 1; pi < s.Points.Count - 1; pi++)
{
DataPoint dp = s.Points[pi];
int y = (int) ay.ValueToPixelPosition(dp.YValues[0]+1); ///*1*
int x0 = (int)ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int)ax.ValueToPixelPosition(s.Points[pi-1].XValue); ///*2*
int x2 = (int)ax.ValueToPixelPosition(dp.XValue);
x1 = Math.Max(x1, x0);
x2 = Math.Max(x2, x0);
using (Pen pen = new Pen(dp.Color, 40) ///*3*
{ StartCap = System.Drawing.Drawing2D.LineCap.Flat,
EndCap = System.Drawing.Drawing2D.LineCap.Flat })
{
g.DrawLine(pen, x1, y, x2, y);
}
}
}
A few notes:
1 : I have decided to move the the series up by one; this is up to you just as using or turning off the y-axis labels or replacing them by custom labels..
2 : Here we use the previous point's x-position!
3 : Note that instead of hard coding a width of 40 pixels you really should decide on a calculated width. This is an example that almost fills up the area:
int width = (int)( ( ay.ValueToPixelPosition(ay.Minimum) -
ay.ValueToPixelPosition(ay.Maximum)) / (chart7.Series.Count + 2));
You can twist is to fill more or less by adding less or more than 2.
I have turned all BorderWidths to 0 so only the drawn lines show.
I got it:
It turned out to actually be pretty easy; I used the Range Column type.
A) Set-up (done once):
plotChart.Series[chanNo].ChartType = SeriesChartType.RangeColumn;
plotChart.Series[chanNo].CustomProperties = "PointWidth=" + noOfFlags;
PointWidth is required to set the relative width of each rectangle so that it fills the entire width of one data point (if too small, there are gaps in the horizontal bar; if too large, there is overlap). noOfFlags is the number of flags shown (in the example shown above, noOfFlags = 4). (By the way the MSDN documentation is wrong: PointWidth is not limited to 2.)
B) Plotting (done for each new data point):
baseLine--;
int barHeight = flagHigh ? 1 : 0;
plotChart.Series[chanNo].Points.AddXY(pointX, baseLine, baseLine + barHeight);
flagHigh is a bool that is equal to the flag being monitored.
baseLine is decremented for each trace. In the example above, baseLine starts at 4, and is decremented down to 0.
Note that for each data point, RangeColumn requires 2 "Y" values: one for the bottom of the rectangle, one for the top; in the code, I set the bottom Y to the bottom of the row that I use for that particular flag, and the top to 1 above the bottom, to give me a height of 1.
I'm working on a Windows Store App (C#) using Bing Maps.
I want to be able to, given collection of Locations (latitude and longitude pairs), determine what the zoom level for the map should be, and what its center point (Location) should be.
From the collection of Location values, I extract the four "extreme" cardinal points that need to be displayed (furthest north, south, east, and west).
IOW, if I want to display pushpins throughout San Francisco, I want to get the zoom level to show just that city and nothing more. If I want to display pushpins scattered across the U.S., ... you get the picture.
This is what I have so far (just a rough draft/pseudocode, as you can see):
Determine the extreme cardinal values of a set of Locations (code not shown; should be trivial). Create an instance of my custom class:
public class GeoSpatialBoundaries
{
public double furthestNorth { get; set; }
public double furthestSouth { get; set; }
public double furthestWest { get; set; }
public double furthestEast { get; set; }
}
...then call these methods, passing that instance:
// This seems easy enough, but perhaps my solution is over-simplistic
public static Location GetMapCenter(GeoSpatialBoundaries gsb)
{
double lat = (gsb.furthestNorth + gsb.furthestSouth) / 2;
double lon = (gsb.furthestWest + gsb.furthestEast) / 2;
return new Location(lat, lon);
}
// This math may be off; just showing my general approach
public static int GetZoomLevel(GeoSpatialBoundaries gsb)
{
double latitudeRange = gsb.furthestNorth - gsb.furthestSouth;
double longitudeRange = gsb.furthestEast - gsb.furthestWest;
int latZoom = GetZoomForLat(latitudeRange);
int longZoom = GetZoomForLong(longitudeRange);
return Math.Max(latZoom, longZoom);
}
Here's where I really get lost, though. How do I determine the zoom level to return (between 1..20) based on these vals? Here's a very rough idea (GetZoomForLat() is basically the same):
// Bing Zoom levels range from 1 (the whole earth) to 20 (the tippy-top of the cat's whiskers)
private static int GetZoomForLong(double longitudeRange)
{
// TODO: What Zoom level ranges should I set up as the cutoff points? IOW, should it be something like:
if (longitudeRange > 340) return 1;
else if (longitudeRange > 300) return 2;
// etc.? What should the cutoff points be?
else return 1;
}
Does anyone have any suggestions or links that can point me to how to implement these functions?
I wrote a blog post on how to do this a while ago here: http://rbrundritt.wordpress.com/2009/07/21/determining-best-map-view-for-an-array-of-locations/
You can use the LocationRect class to set the bounding box, see the MSDN:
http://msdn.microsoft.com/en-us/library/hh846491.aspx
Then you use the Map class and its SetView() method, see the MSDN:
http://msdn.microsoft.com/en-us/library/hh846504.aspx
Here is a code that would work (where map is your map control instance):
var collection = new LocationCollection();
collection.Add(new Location(47.5, 2.75));
collection.Add(new Location(48.5, 2.75));
collection.Add(new Location(43.5, 5.75));
map.SetView(new LocationRect(collection));
So you can use each coordinates of the elements that you want to display on your map in the collection to generate the bounding box and set the view accordingly.
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];