I am using the Charts component in Windows Forms.
I create a straight line
using
chart1.Series["Grenzwert"].Points.Add(new DataPoint(0, y));
chart1.Series["Grenzwert"].Points.Add(new DataPoint(maxwidth, y));
Also I plot a a series of points connected by a line, let's call it curve
.
How do I show everything over straight line
and under curve
filled?
Column fills the whole area, not just above straight line
.
Example:
I have an idea that use SeriesChartType.Range
as follow.
private void UpdateChart(float straight_line, List<DataPoint> curve)
{
float y = straight_line; // YValue of the straight line
var list = curve.ToList(); // Clone the curve
int count = list.Count - 2;
for (int i = 0; i < count; i++) // Calculate intersection point between the straight line and a line between (x0,y0) and (x1,y1)
{
double x0 = list[i + 0].XValue;
double y0 = list[i + 0].YValues[0];
double x1 = list[i + 1].XValue;
double y1 = list[i + 1].YValues[0];
if ((y0 > y && y1 < y) || (y0 < y && y1 > y))
{
double x = (y - y0) * (x1 - x0) / (y1 - y0) + x0;
list.Add(new DataPoint(x, y));
}
}
list.Sort((a, b) => Math.Sign(a.XValue - b.XValue));
chart1.Series[0].Points.Clear();
chart1.Series[0].ChartType = SeriesChartType.Range;
chart1.Series[0].Color = Color.Red;
chart1.Series[0].BorderColor = Color.Cyan;
chart1.ChartAreas[0].AxisX.Minimum = 0;
chart1.ChartAreas[0].AxisX.Interval = 1;
for (int i = 0; i < list.Count; i++)
{
double xx = list[i].XValue;
double yy = list[i].YValues[0];
if (yy > y)
{
chart1.Series[0].Points.AddXY(xx, y, yy);
}
else
{
chart1.Series[0].Points.AddXY(xx, yy, yy);
}
}
chart1.ChartAreas[0].AxisY.StripLines.Add(new StripLine { IntervalOffset = y, Interval = 0, BorderColor = Color.Orange, BorderWidth = 2 });
}
As in the below drawing to judge whether the straight line and a line between (x0,y0) and (x1,y1) intersect, case 1 is (y0 < y && y1 > y)
and case 2 is (y0 > y && y1 < y)
. In case 1 and case 2, they intersect each other. In case 3 and case 4, they don't intersect each other.
This is late and not really short but imo it is the best way to color areas in a chart.
The Lines
and also the Spline
charttypes can be very precisely colored by coding the Paint
event with the right data. The necessary pixel values can be obtained by the axis function ValueToPixelPosition
. See here for another example!
The following code is a little longer because we need to add certain points at the start and end of both the chart and each colored area. Other than that it is very straight forward: Create GraphicsPaths
by adding the pixel coordinates with AddLines
and fill the GraphicsPaths
in the Paint
event.
For testing and for fun I have added a movable HorizontalLineAnnotation
, so I can see how the areas vary when I drag it up and down..:
The Paint
event is rather simple; it refers to a HorizontalLineAnnotation hl
:
private void chart1_Paint(object sender, PaintEventArgs e)
{
double limit = hl.Y; // get the limit value
hl.X = 0; // reset the x value of the annotation
List<GraphicsPath> paths = getPaths(chart1.ChartAreas[0], chart1.Series[0], limit);
using (SolidBrush brush = new SolidBrush(Color.FromArgb(127, Color.Red)))
foreach (GraphicsPath gp in paths)
{ e.Graphics.FillPath(brush, gp); gp.Dispose(); }
}
The code to get the paths is obviously way too long for comfort..:
List<GraphicsPath> getPaths(ChartArea ca, Series ser, double limit)
{
List<GraphicsPath> paths = new List<GraphicsPath>();
List<PointF> points = new List<PointF>();
int first = 0;
float limitPix = (float)ca.AxisY.ValueToPixelPosition(limit);
for (int i = 0; i < ser.Points.Count; i++)
{
if ((ser.Points[i].YValues[0] > limit) && (i < ser.Points.Count - 1))
{
if (points.Count == 0) first = i; // remember group start
// insert very first point:
if (i == 0) points.Insert(0, new PointF(
(float)ca.AxisX.ValueToPixelPosition(ser.Points[0].XValue), limitPix));
points.Add( pointfFromDataPoint(ser.Points[i], ca)); // the regular points
}
else
{
if (points.Count > 0)
{
if (first > 0) points.Insert(0, median(
pointfFromDataPoint(ser.Points[first - 1], ca),
pointfFromDataPoint(ser.Points[first], ca), limitPix));
if (i == ser.Points.Count - 1)
{
if ((ser.Points[i].YValues[0] > limit))
points.Add(pointfFromDataPoint(ser.Points[i], ca));
points.Add(new PointF(
(float)ca.AxisX.ValueToPixelPosition(ser.Points[i].XValue), limitPix));
}
else
points.Add(median(pointfFromDataPoint(ser.Points[i - 1], ca),
pointfFromDataPoint(ser.Points[i], ca), limitPix));
GraphicsPath gp = new GraphicsPath();
gp.FillMode = FillMode.Winding;
gp.AddLines(points.ToArray());
gp.CloseFigure();
paths.Add(gp);
points.Clear();
}
}
}
return paths;
}
It uses two helper functions:
PointF pointfFromDataPoint(DataPoint dp, ChartArea ca)
{
return new PointF( (float)ca.AxisX.ValueToPixelPosition(dp.XValue),
(float)ca.AxisY.ValueToPixelPosition(dp.YValues[0]));
}
PointF median(PointF p1, PointF p2, float y0)
{
float x0 = p2.X - (p2.X - p1.X) * (p2.Y - y0) / (p2.Y - p1.Y);
return new PointF(x0, y0);
}
The HorizontalLineAnnotation
is set up like this:
hl = new HorizontalLineAnnotation();
hl.AllowMoving = true;
hl.LineColor = Color.OrangeRed;
hl.LineWidth = 1;
hl.AnchorDataPoint = S1.Points[1];
hl.X = 0;
hl.Y = 0; // or some other starting value..
hl.Width = 100; // percent of chart..
hl.ClipToChartArea = chart1.ChartAreas[0].Name; // ..but clipped
chart1.Annotations.Add(hl);
You can do this as follows.
- Set the column fill like you did before. Everything will be red.
- Create a new column graph on the same chart.
- Set its values to the same as your jagged line, but capped at the y value of the straight line you already have.
- Set the fill colour for the columns to white. This will block out the red fill for any areas not between the lines.