0

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.

1 Answers1

0

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;
    ...
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • There's a little issue with this, see edit part. It works fine If I use `Add` instead of `Insert`. –  Dec 09 '19 at 11:15
  • Looks like you haven't set the `Y1` Bindings correctly. All lines seem to have `Y1 = 0`. – Clemens Dec 09 '19 at 11:19
  • No it's set. This is so because I've `Collection.Insert(0, ...)` instead of `Collection.Add(...)` –  Dec 09 '19 at 11:22
  • That should obviously not matter. The ItemsControl simply displays the items in the order of the collection, no matter how you have added elements. – Clemens Dec 09 '19 at 11:25
  • I've tested it with `Add` and it works perfectly BUT it become like that if I use `Insert(0,...)`. –  Dec 09 '19 at 11:26
  • 1
    Maybe PreviousData is not refreshed when an item is inserted at the head of the collection. You may add a PreviousPrice member to you item class and bind that instead of the Price property of PreviousData. – Clemens Dec 09 '19 at 11:36
  • 1
    You could also make the PreviousPrice property a nullable double (declared as `double?`) and make the DataTrigger operate directly on that property, with `Value="{x:Null}"`. Now you can drop the AternationCount/Index stuff. – Clemens Dec 09 '19 at 12:05