Search code examples
c#wpfdata-bindinggraphdynamic-data-display

Dynamic Data Display graph of variable number of rectangles


I'm trying to plot a user's input data that will eventually become a series of rectangles (different sizes and positions, not overlapping) in a graph. All examples I've read either only plot lines with a variable number of points or hard-code in the shapes in the XAML. But I don't know how many rectangles the data will need. My ideal case would be to follow MVVM and simply bind from the XAML to an ObservableCollection I can modify, but most examples I see seem to use the code-behind instead, accessing the ChartPlotter directly. Here's what I've got with some simple rectangles drawn and one modified, which works:

VisualizationWindow.xaml

<Window x:Class="View.VisualizationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"
        Title="VisualizationWindow" MinHeight="300" MinWidth="500" Height="300" Width="500">
    <Grid>
        <d3:ChartPlotter Name="Chart">
            <d3:RectangleHighlight Name="Rect1" Bounds="-1,-1.5,.5,2" StrokeThickness="3" Fill="Blue" ToolTip="Blue!"></d3:RectangleHighlight>
            <d3:RectangleHighlight Name="Rect2" Bounds="1,1.5,2,.5" StrokeThickness="1" Fill="Red" ToolTip="Red!"></d3:RectangleHighlight>
        </d3:ChartPlotter>
    </Grid>
</Window>

VisualizationWindow.xaml.cs

public partial class VisualizationWindow : Window
{
    public VisualizationWindow(ListViewModel vm)
    {
        InitializeComponent();
        this.DataContext = vm;

        Chart.Viewport.Visible = new Rect(-1, -1, .5, .5);

        Rect1.Bounds = new Rect(0,0,.3,.3);
    }
}

Documentation on Dynamic Data Display is almost nonexistent. I'd love to know if other libraries can do this more easily if D3 can't do it elegantly.


Solution

  • Adding ItemsSource handling to existing classes.

    It seems like they've done everything to make ChartPlotter only accept IPlotterElements. There's no ItemsSource property, so Children will always return the actual elements. RectangleHighlight's Bounds property is not bindable and the class is sealed, barring any methods to override the property.

    We can derive from the class to "inject" ItemsSource handling. It won't work like the real deal, as there's no non-hackish way to have the Children property reflect the data-binding. But, we can still assign ItemsSource this way.

    We'll need a few things. We'll need the actual ItemsSource property. A way to react to it being set. And if we want binding to dataObjects, a way to handle DataTemplates. I didn't dig into the existing source code yet. However, I did come up with a way to Handle DataTemplates without DataTemplateSelector. But it also won't work with DataTemplateSelector unless you modify my examples.

    This answer assumes you know how binding works, so we'll skip to the initial classes without much hand-holding.

    Xaml first:

    <local:DynamicLineChartPlotter Name="Chart" ItemsSource="{Binding DataCollection}">
        <local:DynamicLineChartPlotter .Resources>
            <DataTemplate DataType{x:Type local:RectangleHighlightDataObject}>
                <d3:RectangleHighlight 
                    Bounds="{Binding Bounds}" 
                    StrokeThickness="{Binding StrokeThickness}"
                    Fill="{Binding Fill}"
                    ToolTip="{Binding ToolTip}"
                />
            </DataTemplate>
        </local:DynamicLineChartPlotter .Resources>
    </local:DynamicLineChartPlotter >
    

    Classes:

    public class RectangleHighlightDataObject
    {
        public Rect Bounds { get; set; }
        public double StrokeThickness { get; set; }
        public Brush Fill { get; set; }
        public String ToolTip { get; set; }
    }
    
    
    public class VisualizationWindow
    {
        public VisualizationWindow() 
        {
             DataCollection.Add(new RectangleHighlightDataObject()
             {
                 Bounds = new Rect(-1,-1.5,.5,2),
                 StrokeThickness = 3,
                 Fill = Brushes.Blue,
                 ToolTip = "Blue!"
             });
    
             DataCollection.Add(new RectangleHighlightDataObject()
             {
                 Bounds = new Rect(1,1.5,2,.5),
                 StrokeThickness = 1,
                 Fill = Brushes.Red,
                 ToolTip = "Red!"
             });
        }
        public ObservableCollection<RectangleHighlightDataObject> DataCollection = 
                 new ObservableCollection<RectangleHighlightDataObject>();
    }
    

    You'll have to use a derived class from ChartPlotter than implements an ItemsSource.

    An example gleaned from a discussion on how to implement dynamic D3 types. I modified to use DataTemplates instead of actual object elements, this is to support databinding par the OP.

    public class DynamicLineChartPlotter : Microsoft.Research.DynamicDataDisplay.ChartPlotter
    {
        public static DependencyProperty ItemsSourceProperty =
                DependencyProperty.Register("ItemsSource",
                                            typeof(IEnumerable),
                                            typeof(DynamicLineChartPlotter),
                                            new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
    
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Bindable(true)]
        public IEnumerable ItemsSource
        {
            get
            {
                return (IEnumerable)GetValue(ItemsSourceProperty);
            }
            set
            {
                if (value == null)
                    ClearValue(ItemsSourceProperty);
                else
                    SetValue(ItemsSourceProperty, value);
            }
        }
    
        private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
            IEnumerable oldValue = (IEnumerable)e.OldValue;
            IEnumerable newValue = (IEnumerable)e.NewValue;
    
            if (e.OldValue != null)
            {
                control.ClearItems();
            }
            if (e.NewValue != null)
            {
                control.BindItems((IEnumerable)e.NewValue);
            }
        }
    
        private void ClearItems()
        {
            Children.Clear();
        }
    
        private void BindItems(IEnumerable items)
        {
            foreach (var item in items)
            {
                var template = GetTemplate(item);
                if (template == null) continue;
    
                FrameworkElement obj = template.LoadContent() as FrameworkElement;
                obj.DataContext = item;
                Children.Add((IPlotterElement)obj);
            }
        }
    
        private DataTemplate GetTemplate(object item)
        {
            foreach (var key in this.Resources.Keys)
            {
                if (((DataTemplateKey)key).DataType as Type == item.GetType())
                {
                    return (DataTemplate)this.Resources[key];
                }
            }
            return null;
        }
    }
    

    Now this is where you hit a brick wall.

    RectangleHighlight Bounds property cannot be data-bound. You also can't derive from them to get around this problem.

    We could hack our way around by pulling out the data template and generating a static RectangleHighlight, but if the data values change, we're sol.

    So, how to fix this?

    Well, we could use attached properties!

    Using Attached Properties

    We'll create a static class that will handle the attached property. It responds to the OnPropertyChanged to manually create and set the real property. Now, this will only work one way. If you happen to change the property, it won't update the attached property. However, this shouldn't be a problem since we should only ever update our data object.

    Add this class

    public class BindableRectangleBounds : DependencyObject
    {
        public static DependencyProperty BoundsProperty = DependencyProperty.RegisterAttached("Bounds", typeof(Rect), typeof(BindableRectangleBounds), new PropertyMetadata(new Rect(), OnBoundsChanged));
    
        public static void SetBounds(DependencyObject dp, Rect value)
        {
            dp.SetValue(BoundsProperty, value);
        }
        public static void GetBounds(DependencyObject dp)
        {
            dp.GetValue(BoundsProperty);
        }
    
        public static void OnBoundsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs args)
        {
            var property = dp.GetType().GetProperty("Bounds");
            if (property != null)
            {
                property.SetValue(dp, args.NewValue, null);
            }
        }
    }
    

    Then change the XAML line from

                    Bounds="{Binding Bounds}" 
    

    to

                    local:BindableRectangleBounds.Bounds="{Binding Bounds}" 
    

    Responding to collection changed.

    So far so good. But the OP noticed that if he made changes to the collection he assigned ItemsSource to, nothing changes in the control. Well that's because we only add children when ItemsSource is assigned to. Now, barring knowing exactly how ItemsControl implements ItemsSource. I know we can get around this by registering to the ObservableCollection's events when the collection changes. I put a simple method of rebinding the controls whenever the collection changes. This will only work if ItemsSource is assigned by an ObservableCollection though. But I would think this would have the same problem with ItemsControl not having an ObservableCollection.

    We're still not reassigning the dataContext however. So don't expect altering Children would correctly rebind. However if you did alter children directly, instead of the ItemsSource in an ItemsControl, you'd lose the binding. So I may very well be on track. The only quirk we lose is that ItemsControl returns the ItemsSource when you refer to Children after setting ItemsSource. I haven't worked around this yet. The only thing I can think of offhand is to hide the children property, but that's not a nice thing to do, and won't work if you refer to the control as a ChartPlotter.

    Add the following

        public DynamicLineChartPlotter()
        {
            _HandleCollectionChanged = new NotifyCollectionChangedEventHandler(collection_CollectionChanged);
        }
    
        private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DynamicLineChartPlotter control = (DynamicLineChartPlotter)d;
            IEnumerable oldValue = (IEnumerable)e.OldValue;
            IEnumerable newValue = (IEnumerable)e.NewValue;
            INotifyCollectionChanged collection = e.NewValue as INotifyCollectionChanged;
            INotifyCollectionChanged oldCollection = e.OldValue as INotifyCollectionChanged;
    
            if (e.OldValue != null)
            {
                control.ClearItems();
            }
            if (e.NewValue != null)
            {
                control.BindItems((IEnumerable)e.NewValue);
            }
            if (oldCollection != null)
            {
                oldCollection.CollectionChanged -= control._HandleCollectionChanged;
                control._Collection = null;
            }
            if (collection != null)
            {
                collection.CollectionChanged += control._HandleCollectionChanged;
                control._Collection = newValue;
            }
        }
    
        void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            ClearItems();
            BindItems(_Collection);
        }
    
        NotifyCollectionChangedEventHandler _HandleCollectionChanged;
        IEnumerable _Collection;