Reputation: 510
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.
Upvotes: 3
Views: 555
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
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