Search code examples
wpfchartsshapes

Drawing Ellipse Line/PolyLine with a double


I have been working with this example to draw bar graph. The bar part is done and it represents the volume/quantity in a transaction. It's an associated price in range 20 - 30. What I want now is to draw points to represent price associated with volumes and connect those points. Two changes I've made in EDIT part of the linked example (1) removed the TextBlock from the DataTemplate of ItemsControl and added an Ellipse instead and (2) edited the canvas to add price/volume axis label. Here's how it looks like now:

enter image description here

How to add those Ellipse in right position and connect those with Line/PolyLine?

EDIT

Here's what I've now in ItemsControl:

<ItemsControl ScrollViewer.CanContentScroll="True"
            Height="135"
            ItemsSource="{Binding RectCollection}"
            Margin="50 0 50 0">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Canvas Width="20">
                <Canvas.LayoutTransform>
                    <ScaleTransform ScaleY="-1"/>
                </Canvas.LayoutTransform>
                <Rectangle Width="18" 
                Margin="0 0 2 0" 
                VerticalAlignment="Bottom"
                Opacity=".5" Fill="LightGray">
                    <Rectangle.Height>
                        <MultiBinding Converter="{StaticResource VConverter}">
                            <Binding Path="ActualHeight"
                                    RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                            <Binding Path="DataContext.HighestPoint"
                                    RelativeSource="{RelativeSource AncestorType=ItemsControl}"/>
                            <Binding Path="Volume"/>
                        </MultiBinding>
                    </Rectangle.Height>
                </Rectangle>

                <Line Stroke="DarkGreen" StrokeThickness="1"
                    X1="10" X2="30"
                    Y2="{Binding PreviousPrice, Converter={StaticResource PConverter}}"
                    Y1="{Binding CurrentPrice, Converter={StaticResource PConverter}}">
                    <Line.Style>
                        <Style TargetType="Line">
                            <Style.Triggers>
                                    <DataTrigger Binding="{Binding Path=PreviousPrice}" Value="{x:Null}">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Line.Style>
                </Line>
                <Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
                    Canvas.Top="{Binding CurrentPrice, Converter={StaticResource PConverter}}"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

    <ItemsControl.Template>
        <ControlTemplate>
            <ScrollViewer
            VerticalScrollBarVisibility="Hidden"
            Background="{TemplateBinding Panel.Background}">
                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </ScrollViewer>
        </ControlTemplate>
    </ItemsControl.Template>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

oh! I forgot to add those ValueConverters, here're those:

public class VolumeConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var height = (double)values[0];
        var higest = (double)values[1];
        var value = (double)values[2];
        return value * height / higest;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class PriceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is double)) return null;
        var price = (double)value;
        var remainingHeight = 90;
        var priceRange = 30 - 20.0;
        return 45 + ((price - 20) * remainingHeight / priceRange);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

and here's how it looks like:

enter image description here

As suggested by @Clemens, I've to have another double?, in case where Insert(0, ... ) is used on ObservableCollection instead of Add(...) to add the last item in first place and removed the AternationCount/Index stuff.


Solution

  • The following example uses a vertically flipped Canvas to invert the y-axis order, so that it goes upwards. So PConverter should return positive y values.

    Besides the Rectangle and Ellipse elements it draws a Line element from the previous data value to the current one by means of RelativeSource={RelativeSource PreviousData} in the value binding. It also uses a DataTrigger on the AlternationIndex to hide the first line.

    <ItemsControl ... AlternationCount="2147483647">
    ...
    
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Canvas Width="20">
                <Canvas.LayoutTransform>
                    <ScaleTransform ScaleY="-1"/>
                </Canvas.LayoutTransform>
    
                <Rectangle Fill="LightGray" Margin="1" Width="18"
                           Height="{Binding Value1, Converter={StaticResource PConverter}}"/>
    
                <Line Stroke="DarkGreen" StrokeThickness="3"
                      X1="-10" X2="10"
                      Y1="{Binding Price,
                           Converter={StaticResource PConverter},
                           RelativeSource={RelativeSource PreviousData}}"
                      Y2="{Binding Price,
                           Converter={StaticResource PConverter}}">
                    <Line.Style>
                        <Style TargetType="Line">
                            <Style.Triggers>
                                <DataTrigger
                                    Binding="{Binding Path=(ItemsControl.AlternationIndex),
                                        RelativeSource={RelativeSource
                                            AncestorType=ContentPresenter}}"
                                    Value="0">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Line.Style>
                </Line>
    
                <Ellipse Fill="Red" Width="6" Height="6" Margin="-3" Canvas.Left="10"
                         Canvas.Top="{Binding Price, Converter={StaticResource PConverter}}"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    

    Since the value converter is now also called for a non-existing value (for PreviousData of the first item), you have to make sure that it checks if the passed value is actually a double:

    public object Convert(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is double)) return 0d;
        ...
    }