2

I was wanting to do a project to make a very generic reusable user control for charting that was pretty much formless. I was hoping to get my feet wet in charting but it seems almost EVERYONE on the planet uses WPF Toolkit or Sparrow Charts for free 3rd party ways to make charts. Does anyone have experience in binding or building a totally isolated based way of doing charting? I was thinking of doing something generic to start like binding a polyline in a canvas and passing. I was curious if anyone else had been down this road and had tips for setting up binding for event raising and potential dependent properties. I was thinking of following an MVVM architecture approach and doing most of the binding to a ViewModel but I would ultimately want to expose properties to update.

Similar to this in concept (UserControl to embed in another View or MainForm):

<StackPanel>
    <Label x:Name="lblCustomDataGridHeader" Content="{Binding TestText}"  HorizontalAlignment="Center" FontSize="24"/>
    <Canvas Height="260" Width="300">
      <Polyline Points="{Binding Points}" Stroke="LightBlue" StrokeThickness="4" />
    </Canvas>
  </StackPanel>

ViewModel Properties:

public ViewModel()
{
    TestText = "Line Chart";
    //Obviously some converter or something else here to accept one or many   lines
    Points = "0,260 10,250 20,245 40,200 50,250 80, 200, 140,100";
}

public string TestText {
    get { return _testText; }
    set 
    {
        _testText = value;
        OnPropertyChanged(NameOf(TestText));
    }
}

private string _points;
public string Points {
    get { return _points; }
    set 
    {
        _points = value;
        OnPropertyChanged(NameOf(Points));
    }
}

EDIT LATER

I have also tried doing a templated control that binds to a class

 <Style TargetType="{x:Type local:LineGraph}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:LineGraph}">
        <Grid>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
          </Grid.RowDefinitions>
          <TextBlock Grid.Row="0" Text="Hello" FontSize="20"/>
          <Border Grid.Row="1" BorderThickness="1" BorderBrush="Black" CornerRadius="15" Margin="10">
            <Canvas Margin="10" x:Name="PART_Canvas">
              <Canvas.LayoutTransform>
                <ScaleTransform ScaleX="1" ScaleY="-1" />
              </Canvas.LayoutTransform>
            </Canvas>
          </Border>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Class (some of this needs to be cleaned up as I was using someone else's implementation and it was in VB.NET and converted):

public class LineGraph : Control, INotifyPropertyChanged
{

    //CONSTRUCTOR
    static LineGraph()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LineGraph), new FrameworkPropertyMetadata(typeof(LineGraph)));
    }

    public event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged;

    public void OnPropertyChanged(string info)
    {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }


    public static readonly DependencyProperty _Trends = DependencyProperty.RegisterReadOnly("Trends", typeof(Collection<ChartDataSegment>), typeof(LineGraph), new PropertyMetadata(new Collection<ChartDataSegment>())).DependencyProperty;
    public Collection<ChartDataSegment> Trends {
        get { return (Collection<ChartDataSegment>)GetValue(_Trends); }
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        dynamic canvas = GetTemplateChild("PART_Canvas") as Canvas;
        if (canvas != null && Trends != null) {
            foreach (void trend_loopVariable in Trends) {
                trend = trend_loopVariable;
                DrawTrend(canvas, trend);
            }
        }
    }

    private void DrawTrend(Canvas drawingCanvas, ChartDataSegment Trend)
    {
        dynamic t = Trend as ChartDataSegment;
        if (t != null && t.Points != null) {
            for (int i = 1; i <= t.Points.Count - 1; i++) {
                dynamic toDraw = new Line {
                    X1 = t.Points(i - 1).X,
                    Y1 = t.Points(i - 1).Y,
                    X2 = t.Points(i).X,
                    Y2 = t.Points(i).Y,
                    StrokeThickness = 2,
                    Stroke = t.LineColor
                };
                drawingCanvas.Children.Add(toDraw);
            }
        }
    }

}

public class ChartDataSegment : DependencyObject
{


    public static readonly DependencyProperty _LineColor = DependencyProperty.Register("LineColor", typeof(Brush), typeof(ChartDataSegment), new PropertyMetadata(null));
    public Brush LineColor {
        get { return (Brush)GetValue(_LineColor); }
        set { SetValue(_LineColor, value); }
    }

    public static readonly DependencyProperty _LineThickness = DependencyProperty.Register("LineThickness", typeof(Thickness), typeof(ChartDataSegment), new PropertyMetadata(null));
    public Thickness PointThickness {
        get { return (Thickness)GetValue(_LineThickness); }
        set { SetValue(_LineThickness, value); }
    }

    public static readonly DependencyProperty _Points = DependencyProperty.Register("Points", typeof(ObservableCollection<Point>), typeof(ChartDataSegment), new UIPropertyMetadata(null));
    public ObservableCollection<Point> Points {
        get { return (ObservableCollection<Point>)GetValue(_Points); }
        set { SetValue(_Points, value); }
    }
}

And implementation in ViewModel:

var lineTrend1 = new ChartDataSegment {
    LineColor = Brushes.Blue,
    Points = new ObservableCollection<Point>({
        new Point {
            X = 1,
            Y = 1
        },
        new Point {
            X = 50,
            Y = 20
        },
        new Point {
            X = 100,
            Y = 100
        },
        new Point {
            X = 150,
            Y = 130
        }
    })
};

var ctrl = new LineGraph();
ctrl.Trends.Add(lineTrend1);

My biggest concern will be not that this can be accomplished but injecting items in on demand not just on instantiation but later after the object is already running to keep updating the lines as needed. EG: Async calls to update a line chart rather than host a static chart that needs to be disposed and then recalled.

My immediate question since Stack Overflow wants specifics on problems is: "Can you easily inject collections(lines) of collections(points) and use a Canvas with a dependent property to self update?"

djangojazz
  • 14,131
  • 10
  • 56
  • 94
  • 2
    This really isn't on topic. Really hard to get yes/no questions to be. If you've never done any UI work, it's **effing hard**. So, unless you want to dedicate a few months *at least* on it, just get a 3rd party library. They're expensive for the same reason divorce is expensive. –  Nov 03 '16 at 15:42
  • So this: "My immediate question since Stack Overflow wants specifics on problems is: "Can you easily inject collections(lines) of collections(points) and use a Canvas with a dependent property to self update?"" Is not specific? Which part is not specific enough? – djangojazz Nov 03 '16 at 15:50
  • It's not snobby, it just has standards. I read that at the bottom, but what good is it to you if I add an answer that says "yes" or "no"? And how do you know I'm correct? Hell, I can tell you from my experience that it's easy to do what you ask, but that's just scratching the surface of control design. But then again, maybe I just suck, and you will find it very easy. This is the reason why your question is not on topic. Bolding parts of it won't make that not true. –  Nov 03 '16 at 16:01

2 Answers2

3

I made a specific type of graph that I needed, and like you, I did not use any third party library. I was looking for it and I found this project project: http://www.codeproject.com/Articles/446888/Custom-Spider-or-Radar-Chart-Control

They make a chart similar to the following:

enter image description here

The sources are available for download, and it does not use third-party code so you can easily review all they do.

To create the graph is only necessary to do:

<WpfCharts:SpiderChart Title="Spider chart"  
                   Lines="{Binding Lines}" 
                   Axis="{Binding Axes}" 
                   Minimum="0" 
                   Maximum="1" 
                   Ticks="5" 
                   ShowLegend="True" 
                   LegendLocation="BottomRight" 
                   LegendBackgroundColor="Aquamarine"/> 

Which as you can see already it's binding the properties. I'm already using it in a MVVM project.

I hope it will guide you in the way of what you're looking for.

Josep B.
  • 587
  • 1
  • 6
  • 16
  • This is EXACTLY what I was looking for. Thanks! They did the similar thing I was thinking of in my second instance. Setting up a style and then having dependencies trigger. And it is self contained with the library which is even better. – djangojazz Nov 04 '16 at 14:50
  • I'm glad that helped. – Josep B. Nov 04 '16 at 18:40
0

Yes you can create a charting control, a simple one with one kind of "Chart", the real work starts when you want to extend that piece of code, then you have to remember that at some point your requirements can change and the design you started with will not be good anymore. Personally I think this would be a good exercise to take on. But you need to remember that the data you will receive will change, nobody will want to pass Collection of Points, I personally would like to pass a collection of integers or floats or decimals. Pretty much whatever tickles my fancy. At this point you will start to scrach your head and think of another way of implementing all that logic again. That's my friend those few months down the line. I am not trying to discourage you from taking this task but your starting point is bound to change in the near future.
BTW Listen to Will he knows what he is talking about.

XAMlMAX
  • 2,268
  • 1
  • 14
  • 23
  • Will did not give an answer other than: "Coding is hard, use a third party." If I can make a chart and bind to it(my chart is working just fine I gave above) I just need a few more steps to see if can get it to self update easily from an API or other means. I know it's an interesting problem, but that is why I asked. It just upsets me a little bit that SO is becoming: "Too vague!!! You are getting into asking questions about architectural choices and techniques as well as a 2 + 2 = ?". If I knew how to do it all I wouldn't be here. I got a start I was just curious for the self updating. – djangojazz Nov 03 '16 at 22:54
  • I don't know what it is either but this 'too vague' zealotry is increasing to the point that anything that borders on discussions of: "I am doing it this way and was thinking of this, what do you think?" is getting rejected. I notice it from other users leaving or saying it too. – djangojazz Nov 03 '16 at 22:58
  • I find it amusing the guidelines for SO are this:'software tools commonly used by programmers; and is a practical, answerable problem that is unique to software development'. My question: Can you easily inject collections(lines) of collections(points) and use a Canvas with a dependent property to self update?" Seems pretty straightforward. Not as well defined as this question obviously: http://stackoverflow.com/questions/332365/how-does-the-sql-injection-from-the-bobby-tables-xkcd-comic-work/332367#332367 – djangojazz Nov 03 '16 at 23:03
  • Really my answer is here to provide a perspective of my approach. As I said I am not discouraging you from this idea. BTW when it comes to `UserControls` there usually is not ViewModel attached to it, all of the logic is done in the code behind, `would you like to populate a ViewModel of a ListView?` no you wouldn't and neither would I. keep the `UserControl` as self contained as possible use `DependencyProperties` so you can utilize binding when using that Control. – XAMlMAX Nov 03 '16 at 23:04
  • To be honest one of the reasons I asked is I don't get why it is better to use Dependent Properties rather than direct binding or a converter. It seems to me to be more code that I have read you can do more with but I am not set on one way or the other. Typically I do all the business logic and data from an API service or database in the ViewModel and animation and other things specifically in the code behind. It is harder to test things IMHO if you mix code behind into your UI patterns. – djangojazz Nov 03 '16 at 23:10
  • That's not what I meant. I meant to create a `UserControl` that will take that collection of `Point`s from your ViewModel, there is a reason why `Control`s inherit DataContext. Create a collection property in your UserControl so you can use it like this in your xaml `` does that makes sense? – XAMlMAX Nov 03 '16 at 23:14
  • If I do this , the items are already binding as it is capturing the datacontext of the parent already by default. My question is more along the lines of when people do something like: "DependencyProperty.Register("Points", GetType(ObservableCollection(Of Point)), GetType(ChartDataSegment), New UIPropertyMetadata(Nothing))" and put that in a class they expose through a naming does that really offer more? I was just curious as I was talking to another developer and they love this stuff, and I am not getting what is so great about it. – djangojazz Nov 03 '16 at 23:20
  • Anyways if I don't get anything or this gets closed I will give my own answer. I was playing with an X and Y implementation. X is a user control bound with properties of the text and polyline to a viewmodel in the example above. Another one is completely in the default xaml like: – djangojazz Nov 03 '16 at 23:22
  • My bad for using StackPanel consider when it is used in a Grid. But I do like your idea and I am looking forward to see what you come up with. I might actually give it a go myself, however I will use the `DependencyProperty` just to make a point here :-) P.S. I will make a Bar Chart. – XAMlMAX Nov 03 '16 at 23:36