SciChart® the market leader in Fast WPF Charts, WPF 3D Charts, iOS Chart, Android Chart and JavaScript Chart Components

0
0

Hello,
I am working on a chart that contains a number of StackedColumnRenderableSeries. Each series has a unique StackedGroupId set so all of the columns display side-by-side (“grouped” mode). I have attached a screenshot of what the chart currently looks like.

The Y-values in the chart are each an average of several collected data values, so I would like to be able to include the standard deviations as error bars drawn over each column, with the center at the top of the column and the high and low values set to center+SD and center-SD.

What would be the best approach to do this? The first thing that comes to mind for me is to create a new class StackedColumnRenderableSeriesWithErrorBars which derives from StackedColumnRenderableSeries, override InternalDraw, and add the drawing of error bars to InternalDraw after calling base.InternalDraw. Of course this would only work as long as the DataSeries is an HlcDataSeries instead of an XyDataSeries.

I also considered an approach that involved overriding/modifying FastErrorBarsRenderableSeries to essentially add a “grouped” mode, but I’m not sure there’s a good way to add that.

I believe the StackedColumnRenderableSeriesWithErrorBars approach will work, but I want to make sure I’m not missing a simpler approach that I could take instead. Any help would be greatly appreciated.

Thank you!

UPDATE: I have it working to the point where it draws the error bars, but the X coordinate is always in the middle of the group, rather than being centered in the middle of the column. I assume I need to make use of the Wrapper (IStackedColumnsWrapper) in the StackedColumnRenderableSeries to shift the X coordinate, but I’m having a little trouble figuring out exactly how. Here is what I have thus far:

public class StackedColumnRenderableSeriesWithErrorBars : StackedColumnRenderableSeries
{
    public bool ShowErrorBars { get; set; } = false;

    protected override void InternalDraw(IRenderContext2D renderContext, IRenderPassData renderPassData)
    {
        base.InternalDraw(renderContext, renderPassData);

        // Now that the main drawing of the StackedColumnRenderableSeries is done, draw the error bars (if applicable).
        if (ShowErrorBars)
        {
            // The resampled data for this render pass
            // Don't try to draw error bars if the PointSeries isn't an HlcPointSeries.
            if (renderPassData.PointSeries is not HlcPointSeries dataPointSeries)
            {
                return;
            }
            ICoordinateCalculator<double> xCalc = renderPassData.XCoordinateCalculator;
            ICoordinateCalculator<double> yCalc = renderPassData.YCoordinateCalculator;

            // Iterate over the point series
            for (int i = 0; i < dataPointSeries.Count; i++)
            {
                // Get the values
                double x = dataPointSeries.XValues[i];
                double high = dataPointSeries.HighValues[i];
                double low = dataPointSeries.LowValues[i];

                // Transform to coordinate
                double xCoord = xCalc.GetCoordinate(x);
                double highCoord = yCalc.GetCoordinate(high);
                double lowCoord = yCalc.GetCoordinate(low);

                // TODO: Use Wrapper to transform the X coordinate based on where in the group this column is.

                // TODO: Set cap width based on column width
                double capWidth = 10;

                using IPen2D pen = renderContext.CreatePen(Colors.Black, true, 2);
                // Draw vertical line
                renderContext.DrawLine(pen, new Point(xCoord, highCoord), new Point(xCoord, lowCoord));
                // Draw top cap
                renderContext.DrawLine(pen, new Point(xCoord - (capWidth / 2), highCoord), new Point(xCoord + (capWidth / 2), highCoord));
                // Draw bottom cap
                renderContext.DrawLine(pen, new Point(xCoord - (capWidth / 2), lowCoord), new Point(xCoord + (capWidth / 2), lowCoord));
            }
        }
    }
}
Version
6.4
Images
  • You must to post comments
0
0

Answering my own question with the solution I came up with. Had to change things up a bit, implemented a new DrawingProvider which does most of the work. Also had to use reflection to get the StackedGroupInfo[] array which I used to calculate the X coordinate to draw each error bar at.

I have attached an image with what my chart looks like now. This pretty much resolves the issue, but I wanted to make sure the solution makes sense. I’m especially hesitant about how I am using the StackedGroupInfo[] array – given that it relies on a private field I know it could be broken by a subsequent change to StackedColumnSeriesDrawingProvider.

public class StackedColumnRenderableSeriesWithErrorBars : StackedColumnRenderableSeries
{
    public StackedColumnRenderableSeriesWithErrorBars() : base()
    {
        // Replace the StackedColumnSeriesDrawingProvider with a StackedColumnSeriesWithErrorBarsDrawingProvider (defined below).
        // This DrawingProvider inherits from StackedColumnSeriesDrawingProvider, but also draws error bars if the source series has ShowErrorBars
        // set to true.
        DrawingProviders = new List<ISeriesDrawingProvider> { new StackedColumnSeriesWithErrorBarsDrawingProvider(this) };
    }

    public static readonly DependencyProperty ShowErrorBarsProperty =
        DependencyProperty.Register("ShowErrorBars", typeof(bool), typeof(StackedColumnRenderableSeriesWithErrorBars),
            new PropertyMetadata(false, PropertyChangedCallback));

    public bool ShowErrorBars
    {
        get { return (bool)GetValue(ShowErrorBarsProperty); }
        set { SetValue(ShowErrorBarsProperty, value); }
    }

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        OnInvalidateParentSurface(d, e);
    }

    public override SeriesInfo GetSeriesInfo(HitTestInfo hitTestInfo)
    {
        //base.GetSeriesInfo(hitTestInfo);
        return new HlcSeriesInfo(this, hitTestInfo);
    }
}

public class StackedColumnSeriesWithErrorBarsDrawingProvider : StackedColumnSeriesDrawingProvider
{
    public StackedColumnSeriesWithErrorBarsDrawingProvider(StackedColumnRenderableSeriesWithErrorBars renderableSeries) :
        base(renderableSeries)
    {

    }

    public override void OnDraw(IRenderContext2D renderContext, IRenderPassData renderPassData)
    {
        base.OnDraw(renderContext, renderPassData);

        if ((RenderableSeries as StackedColumnRenderableSeriesWithErrorBars).ShowErrorBars)
        {
            // The resampled data for this render pass
            // Don't try to draw error bars if the PointSeries isn't an HlcPointSeries.
            if (renderPassData.PointSeries is not HlcPointSeries dataPointSeries)
            {
                return;
            }
            ICoordinateCalculator<double> xCalc = renderPassData.XCoordinateCalculator;
            ICoordinateCalculator<double> yCalc = renderPassData.YCoordinateCalculator;

            // Get the value of the private StackedGroupInfo[] array from the base StackedColumnSeriesDrawingProvider.
            // The information from the StackedGroupInfo objects will allow us to calculate the X coordinate of the column
            // so we can draw the error bar over it.
            FieldInfo[] fields = typeof(StackedColumnSeriesDrawingProvider).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
            FieldInfo stackedGroupInfoField = fields.FirstOrDefault(f => f.FieldType == typeof(StackedGroupInfo[]));
            StackedGroupInfo[] stackedGroupInfo = (StackedGroupInfo[])stackedGroupInfoField.GetValue(this);

            // Iterate over the point series
            for (int i = 0; i < dataPointSeries.Count; i++)
            {
                // Get the values
                double x = dataPointSeries.XValues[i];
                double high = dataPointSeries.HighValues[i];
                double low = dataPointSeries.LowValues[i];

                // Transform to coordinate
                double xCoord = xCalc.GetCoordinate(x);
                double highCoord = yCalc.GetCoordinate(high);
                double lowCoord = yCalc.GetCoordinate(low);

                // Transform xCoord based on grouping
                double columnWidth = RenderableSeries.Wrapper.GetSeriesBodyWidth(RenderableSeries, i);
                double totalWidth = columnWidth * stackedGroupInfo[i].Count;
                // Shift all the way to the left, then shift right by half a column width, then shift right by full column width multiplied by
                // the group index. This will put the X coordinate in the middle of the correct column.
                double columnShift = (-totalWidth / 2) + (columnWidth / 2) + (stackedGroupInfo[i].Index * columnWidth);
                xCoord += columnShift;

                // Set cap width to 50% of the column width, but with a max width of 20 since any wider looks weird.
                double capWidth = Math.Min(columnWidth * 0.5, 20);

                using IPen2D pen = renderContext.CreatePen(Colors.Black, true, 2);
                // Draw vertical line
                renderContext.DrawLine(pen, new Point(xCoord, highCoord), new Point(xCoord, lowCoord));
                // Draw top cap
                renderContext.DrawLine(pen, new Point(xCoord - (capWidth / 2), highCoord), new Point(xCoord + (capWidth / 2), highCoord));
                // Draw bottom cap
                renderContext.DrawLine(pen, new Point(xCoord - (capWidth / 2), lowCoord), new Point(xCoord + (capWidth / 2), lowCoord));
            }
        }
    }
}
Images
  • You must to post comments
Showing 1 result
Your Answer

Please first to submit.