Chart : displaying boolean flags in a time plot as

2019-09-18 14:35发布

问题:

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

回答1:

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.



回答2:

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.