Search code examples
c#wpfchartsitemscontrol

ItemsControl Bar Chart Scaling of the bars


EDIT: added code

Also since the DateTimes are not really Datetimes (sTrings in hh:mm:ss format) i decided to just use Strings instead and use TimeSpan to retrieve the totalMinutes.

<ObjectDataProvider x:Key="odpLbGrafiek" ObjectType="{x:Type myClasses:GrafiekBar}" MethodName="GetDataGrafiek"/>

    <DataTemplate x:Key="GrafiekItemTemplate">
        <Border Width="Auto" Height="Auto">
            <Grid>
                <Rectangle StrokeThickness="0" Height="30"  
                           Margin="15" 
                           HorizontalAlignment="Right" 
                           VerticalAlignment="Bottom"
                           Width="{Binding Value}"
                           Fill="{Binding Fill}">
                    <Rectangle.LayoutTransform>
                        <ScaleTransform ScaleX="20" />
                    </Rectangle.LayoutTransform>
                </Rectangle>
            </Grid>
        </Border>
    </DataTemplate>

The Fill actually gives the size on the bar of the bar chart iself.

The itemsControl:

<ItemsControl x:Name="icGrafiek"  
            Margin="20,3,0,0" 
            ItemsSource="{Binding Source={StaticResource odpLbGrafiek}}"
            ItemTemplate="{DynamicResource GrafiekItemTemplate}" 
            RenderTransformOrigin="1,0.5" HorizontalAlignment="Left" VerticalAlignment="Top" Grid.RowSpan="6">
            <ItemsControl.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleY="-1" ScaleX="1"/>
                    <SkewTransform AngleY="0" AngleX="0"/>
                    <RotateTransform Angle="180"/>
                    <TranslateTransform/>
                </TransformGroup>
            </ItemsControl.RenderTransform>
        </ItemsControl>

The following method gets called in the databinding. There the bar.Value gives the value for the Width value in the datatemplate that gives the size of a bar.

    public ObservableCollection<GrafiekBar> GetDataGrafiek()
    {
        var converter = new System.Windows.Media.BrushConverter();

        Double maxValueStilstanden = GetLargestValueStilstanden();

        TimeSpan tsMaxValue = TimeSpan.Parse(maxValueStilstanden.ToString());
        totalMinutesMaxValue = tsMaxValue.TotalMinutes; 

        //calculate % of stilstanden Values
        foreach(String t in stilStandenList)
        {
            TimeSpan ts = TimeSpan.Parse(t);
            Double totalMin = ts.TotalMinutes;

            totalMin = totalMin / totalMinutesMaxValue * 100;

            valuesChartPercentage.Add(totalMin);
        }

        for (int j = 0; j < valuesChartPercentage.Count; j++)
        {
            GrafiekBar bar = new GrafiekBar();
            bar.Value = valuesChartPercentage[j];
            bar.Fill = converter.ConvertFromString(kleuren[j]) as Brush;
            listGrafiek.Add(bar);
        }

        return listGrafiek;
    }

Another problem is actually the Width (size of the bar). I actually have to do * 10000 to get any visual of the bar itself.


I am using a ItemsControl that is styled so it looks like a bar chart.

So for example a array with 5 values going from 1 to 5 creates 5 bars with different bar sizes and with each a different colors. Much alike the following example http://www.c-sharpcorner.com/uploadfile/mahesh/bar-chart-in-wpf/

The problem:

My problem is with the scale sizing of the bars (the width property in this case, so int/double value are required).

  • My program records several DateTimes in HH:mm:ss format
  • The bars should be scaled on these datetimes

For example i could have a bar with 01:22:11 or a bar with 00:01:11 up to a max amount of 6.

What would be the best approach of scaling these DateTime values to a certain double value? This value will be used to give the size of a bar on the chart.

Guess i am looking for some kind of calculation that calculates all my values just the same, so i do not suddenly get a value that is insanely large and goes out of my UI.

The most clean solution would be that all bars are compared to each other and when one changes the other one grows / shrinks, but that is not required as of yet though unless its not as complicated as it sounds.

The bar char itself does not need to be overly precise, it only serves to get a general picture of the situation. The exact values will be written in a database.

Any suggestions are most welcome!

Thanks PeterP.


Solution

  • I would pick a base date, and make your collection graph the number of days/hours/minutes between the base date and the data date

    You might even be able to do this with a converter, where you pass the base date in as a converter parameter.

    Your two example dates (01:22:11 and 00:01:11) are actually times, so in that case I'd just graph the number of minutes since 0, so your actual data values to graph would be 82 and 1

    In response to your edit asking about scaling, you would be graphing everything as a percentage. In that case, take the largest number and graph every other number based on a percentage of the largest number.

    So using your two example times, I would convert them to the numbers 82 and 1, take the larger number (82) as 100%, and return a list which contains the percentage of every number to 82, so the return list would contain 100% and 1.2% (1/82).

    You can still do this in either the ViewModel or an ItemsSource Converter (converter would take entire list as a parameter, and return an entire list for the return value)


    Edit

    In response to your comments below, here is how I would set it up using a Converter on the ItemsSource. A converter simply takes a data value, and converts it into another value which is specific to the display UI only.

    The initial XAML would look like this:

    <ItemsControl ItemsSource="{Binding MyCollection, 
        Converter="{StaticResource MyTimeConverter}}" />
    

    where MyCollection is an ObservableCollection<DateTime>, and MyTimeConverter does the following:

    1. Cast the passed in value as ObservableCollection<DateTime>, since all converter parameters are passed in as object
    2. Loop through the collection and figure out the largest time
    3. Take the largest time in the collection, and figure out how many minutes it has. This will be your 100% value that you will base all the other times off of, so store the number of minutes your largest time has in a variable
    4. Create a new List<decimal> for the return value
    5. Loop through starting collection. For each time in the collection, divide it by the "100% value" that you stored in step 3, which will give you the percentage of how long that bar should be compared to the largest value. Add this percentage to the return List<decimal>
    6. Return List<decimal> to the ItemsControl. This List<decimal> will be used as the ItemsSource instead of the actual ObservableCollection<DateTime>

    This means that your ItemsControl is now bound to a collection of decimals, where one value is 100% and will take the full width of the screen, all other values are scaled to the maximum value.

    Regarding your question about using a timer to update the collection, your timer should update the ObservableCollection<DateTime> called MyCollection that the ItemsControl is bound to. The timer should not know or care about the converter code at all.

    For example, if your timer wants to re-create MyCollection with a whole new set of times, then it can, and the UI will automatically re-run the converter code and update the bar graph since ObservableCollections will tell the UI when the collection has changed and the UI needs to update.

    As for the "base date" I was referring to in my comments below, if you were graphing a set of dates instead of times, you would want a baseline date so your graph doesn't stretch back to 1/1/0001. You don't want to use the minimum date since that will result in your lowest value showing up as 0 in your bar graph, so you would pass the Converter a specific date to use as the starting point in your graph. If the base date was 1/1/12 and your largest date was 3/1/12, then your graph would stretch from 1/1/12 to 3/1/12.

    The base date would be used in step 3 and 5 of the converter. For example, instead of getting the number of minutes in the time, you might get the number of days between the base date and the data date.

    You could also calculate the base date in the converter, such as 10 days before the lowest date, although that might skew the graph more than you'd prefer depending on the data.