Timmoth
Timmoth

Reputation: 510

How can i plot a collection of data points on a canvas using data bindings?

I have a collection of data points which store an X and a Y value along with two coordinates: The pixel location of the point itself and of the next point.

I then have an ItemsControl which is bound to the collection and draws a line connecting the current point to the next point forming a line chart of all the data points stored in the collection.

  <ItemsControl x:Name="GraphCanvas"
            ItemsSource="{Binding LineChartData}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Canvas>
                            <Canvas.Resources>
                                <local:BindingProxy x:Key="proxy" Data="{Binding}" />
                            </Canvas.Resources>
                            <Path Stroke="Black" StrokeThickness="1">
                                <Path.Data>
                                    <GeometryGroup>
                                        <PathGeometry>
                                            <PathFigure StartPoint="{Binding Source={StaticResource proxy}, Path=Data.CurrentPoint}">
                                                <PathFigure.Segments>
                                                    <LineSegment Point="{Binding Source={StaticResource proxy}, Path=Data.NextPoint}"/>
                                                </PathFigure.Segments>
                                            </PathFigure>
                                        </PathGeometry>
                                    </GeometryGroup>
                                </Path.Data>
                            </Path>
                        </Canvas>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

It all works alright however I want to know if there is a better way to do this as when I have to resize my control i have to loop over each of the datapoints and re calculate their current point and next point like so:

    Point currentPoint;
    Point nextPoint = getCanvasPoint(lineChartData[0]);
    for (int i = 1; i < lineChartData.Count; i++)
    {
        var dataPoint = lineChartData[i];
        currentPoint = nextPoint;
        nextPoint = getCanvasPoint(dataPoint);

        dataPoint.CurrentPoint = currentPoint;
        dataPoint.NextPoint = nextPoint;
    }

Which is a rather slow process and makes the resizing very jumpy. I would like to know if there is a better way for me to bind a list of X & Y values to an itemscontrol so that i can plot them onscreen.

This is how it looks: enter image description here

Upvotes: 3

Views: 555

Answers (2)

Timmoth
Timmoth

Reputation: 510

I used Clemens comment to build my solution here is how i did it:

I bound the Points property of a PolyLineSegment to a collection of points in my ViewModel. Then i used a transform group to scale and translate the path to fit the axis.

Here is the code:

   <Canvas Background="Transparent" Grid.Row="1" Grid.Column="2" x:Name="GraphCanvas">
        <Path Stroke="Black" StrokeThickness="1">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Transform>
                        <TransformGroup>
                            <ScaleTransform ScaleX="{Binding xScale}" ScaleY="{Binding yScale}"/>
                            <TranslateTransform Y="{Binding yAxisTranslation}"/>
                        </TransformGroup>
                    </PathGeometry.Transform>
                    <PathGeometry.Figures>
                        <PathFigureCollection>
                            <PathFigure StartPoint="{Binding FirstPoint}">
                                <PathFigure.Segments>
                                    <PathSegmentCollection>
                                        <PolyLineSegment Points="{Binding Points}"/>
                                    </PathSegmentCollection>
                                </PathFigure.Segments>
                            </PathFigure>
                        </PathFigureCollection>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
        <Path Stroke="Black" StrokeThickness="1">
            <Path.Data>
                <PathGeometry>
                    <PathGeometry.Figures>
                        <PathFigureCollection>
                            <PathFigure StartPoint="0,0">
                                <LineSegment Point="{Binding OriginPoint}"/>
                                <LineSegment Point="{Binding xAxisEndPoint}"/>
                            </PathFigure>
                        </PathFigureCollection>
                    </PathGeometry.Figures>
                </PathGeometry>
            </Path.Data>
        </Path>
    </Canvas>

I really like this solution and it seems a considerable amount faster then my original code especially when resizing.

Upvotes: 0

heltonbiker
heltonbiker

Reputation: 27615

I have been using MultiBinding for that.

In the view, I bind to:

<Grid x:Name="root" RenderTransformOrigin="0.5 0.5">
    <Grid.RenderTransform>
        <ScaleTransform ScaleY="-1"/>
    </Grid.RenderTransform>
    <Path Stroke="Brown" StrokeThickness="0.5"
          Stretch="Fill"
          StrokeEndLineCap="Round" StrokeLineJoin="Round">
        <Path.Data>
            <MultiBinding Converter="{StaticResource SinalToGeometryConverter}">
                <Binding ElementName="root" Path="ActualWidth"/>
                <Binding ElementName="root" Path="ActualHeight"/>
                <Binding Path="Samples"/>
            </MultiBinding>
        </Path.Data>
    </Path>
</Grid>

Then in the converter I generate a Path, more or less like this:

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Any(v => v == null) &&
            values.Any(v => v == DependencyProperty.UnsetValue))
            return null;

        double viewportWidth = (double)values[0];
        double viewportHeight = (double)values[1];

        IEnumerable<int> samples= values[2] as IEnumerable<int>;

        var sb = new StringBuilder("M");
        int x_coord = 0; // could be taken from sample if available
        foreach (var y_coord in samples)
            sb.AppendFormat(" {0} {1}", x_coord++, y_coord);

        var result = Geometry.Parse(sb.ToString());

        return result;
    }

Then, each time one of the bound things change, due to datasource change or viewport change, you get a brand new Path. This is relatively quick if you have less datapoints than your plotterControl.ActualWidth.

Upvotes: 2

Related Questions