I was wondering if the following features are possible with the NetAdvantage line chart, and if so how to use it.We use line charts to show a variable amount of data across a variable time domain. For instance, 3 years worth of patient temperature data. In some periods, 1 measurement per month can have been recorded. In another period, a measurement would have been done every 2 hours. The user looking at the graph might be interested in the 3-year trend, or the data of the last 48 hours. Exactly how much data each patient has and the timespan in which it occurs differs for each patient. The user is presented with the entire data set, and will do plenty of zooming and scrolling.One problem we run into is how to present the x-axis labels. We need:1. Dynamic x-axis text labels. When looking at a timespan of 3 years we do not want the time included in the text label. When looking at a timespan of hours, we do not want to repeat the date in each x-axis label.2. Visual grouping of parts of the x-axis, so that we can for instance specify the date only once for multiple ticks that occur on that date (but at different times). Something like the attached (google) example. How the grouping occurs should be dynamic and depend on zoom level (i.e. days, weeks, months, years).
Preferably we would use a purely declarative method (we use MVVM and try to keep the code behind to a minimum), we want to avoid having to do this programmatically.Are the described scenarios possible with NetAdvantage? Is there documentation describing how to do this?
An issue I'm running into with your code example is the following:
When zooming/scrolling the example works, but when the chart is resized (i.e. HorizontalAlignment is set to Stretch), when MeasureOverride is triggered, all the DateLabels have a Date of DateTime.MinValue
That's exactly what I'm looking for, thanks!
Some screenshots from this sample:
-Graham
here's an example of how you could go about creating this kind of labeling with the xamDataChart:
<Window.Resources> <local:TestData x:Key="data" /> </Window.Resources> <Grid> <Grid x:Name="LayoutRoot" Background="White"> <igChart:XamDataChart x:Name="theChart" HorizontalZoomable="True" VerticalZoomable="True" HorizontalZoombarVisibility="Visible" VerticalZoombarVisibility="Visible"> <igChart:XamDataChart.Axes> <igChart:NumericYAxis x:Name="yAxis" /> <igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}"> <igChart:CategoryXAxis.Label> <DataTemplate> <local:DateLabel Date="{Binding Item.Date}"/> </DataTemplate> </igChart:CategoryXAxis.Label> </igChart:CategoryXAxis> </igChart:XamDataChart.Axes> <igChart:XamDataChart.Series> <igChart:LineSeries x:Name="series" ItemsSource="{StaticResource data}" XAxis="{Binding ElementName=xAxis}" YAxis="{Binding ElementName=yAxis}" ValueMemberPath="Value" /> </igChart:XamDataChart.Series> </igChart:XamDataChart> </Grid>
with code behind:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } public class TestData : ObservableCollection<TestDataItem> { private static Random _rand = new Random(); public TestData() { DateTime now = DateTime.Now; DateTime dt = DateTime.Now; double curr = 10.0; while (dt < now.AddYears(2)) { if (_rand.NextDouble() < .5) { curr += _rand.NextDouble() * 2.0; } else { curr -= _rand.NextDouble() * 2.0; } Add(new TestDataItem() { Date = dt, Value = curr }); dt = dt.AddHours(.5); } } } public class TestDataItem { public DateTime Date { get; set; } public double Value { get; set; } } public class DateLabel : ContentControl { private StackPanel _stack = new StackPanel(); private TextBlock _firstLine = new TextBlock(); private TextBlock _secondLine = new TextBlock(); public DateLabel() { _firstLine.HorizontalAlignment = HorizontalAlignment.Center; _secondLine.HorizontalAlignment = HorizontalAlignment.Center; _stack.Children.Add(_firstLine); _stack.Children.Add(_secondLine); Content = _stack; } public static readonly DependencyProperty DateProperty = DependencyProperty.Register("Date", typeof(DateTime), typeof(DateLabel), new PropertyMetadata(DateTime.MinValue, (o, e) => (o as DateLabel) .OnDateChanged(e.OldValue, e.NewValue))); private void OnDateChanged(object oldValue, object newValue) { } public DateTime Date { get { return (DateTime)GetValue(DateProperty); } set { SetValue(DateProperty, value); } } protected override Size MeasureOverride(Size constraint) { Update(); return base.MeasureOverride(constraint); } private void Update() { _firstLine.Text = ""; _secondLine.Text = ""; var scale = DetermineAxisScale(); if (scale == AxisScale.Unknown) { return; } var displayDifferentiator = ShouldDisplayDifferentiator(scale); var firstListFormat = GetFirstLineFormat(scale); var secondLineFormat = GetSecondLineFormat(scale); var firstText = Date.ToString(firstListFormat); var secondText = Date.ToString(secondLineFormat); _firstLine.Text = firstText; if (displayDifferentiator && scale != AxisScale.Years && !string.IsNullOrEmpty(secondLineFormat)) { _secondLine.Text = secondText; } } private string GetSecondLineFormat(AxisScale scale) { switch (scale) { case AxisScale.Years: return ""; break; case AxisScale.Months: return "yyyy"; break; case AxisScale.Days: if (ShouldDisplayDifferentiator(AxisScale.Months)) { return "MM/yyyy"; } else { return "MM"; } break; case AxisScale.Hours: if (ShouldDisplayDifferentiator(AxisScale.Months)) { return "MM/dd/yyyy"; } else if (ShouldDisplayDifferentiator(AxisScale.Days)) { return "MM/dd"; } else if (ShouldDisplayDifferentiator(AxisScale.Hours)) { return "dd"; } else { return ""; } break; case AxisScale.Minutes: if (ShouldDisplayDifferentiator(AxisScale.Months)) { return "MM/dd/yyyy"; } else if (ShouldDisplayDifferentiator(AxisScale.Days)) { return "MM/dd"; } else if (ShouldDisplayDifferentiator(AxisScale.Hours)) { return "dd"; } else { return ""; } break; } return ""; } private string GetFirstLineFormat(AxisScale scale) { switch (scale) { case AxisScale.Years: return "yyyy"; break; case AxisScale.Months: return "MMM"; break; case AxisScale.Days: return "dd"; break; case AxisScale.Hours: return "hh:mm"; break; case AxisScale.Minutes: return "hh:mm:ss"; break; } return ""; } private bool ShouldDisplayDifferentiator(AxisScale scale) { var sorted = GetSortedLabels(); Func<DateTime, int> getValue = (d) => 0; switch (scale) { case AxisScale.Years: getValue = (d) => 0; break; case AxisScale.Months: getValue = (d) => d.Year; break; case AxisScale.Days: getValue = (d) => d.Month; break; case AxisScale.Hours: getValue = (d) => d.Day; break; case AxisScale.Minutes: getValue = (d) => d.Hour; break; } int last = -1; foreach (var item in sorted) { var val = getValue(item.Date); if (last != val) { last = val; if (item == this) { return true; } } if (item == this) { break; } } return false; } private IEnumerable<DateLabel> GetSortedLabels() { var axisPanel = (this.Parent as FrameworkElement).Parent as Panel; var dateLabels = from item in axisPanel.Children.OfType<ContentControl>() where item.Content is DateLabel select (DateLabel)item.Content; var sorted = from item in dateLabels orderby item.Date select item; return sorted; } private AxisScale DetermineAxisScale() { var sorted = GetSortedLabels(); DateTime firstDate = sorted.First().Date; DateTime lastDate = sorted.Last().Date; if (!HasRepeats(AxisScale.Years, sorted)) { return AxisScale.Years; } if (!HasRepeats(AxisScale.Months, sorted)) { return AxisScale.Months; } if (!HasRepeats(AxisScale.Days, sorted)) { return AxisScale.Days; } if (!HasRepeats(AxisScale.Hours, sorted)) { return AxisScale.Hours; } return AxisScale.Minutes; } private bool HasRepeats(AxisScale scale, IEnumerable<DateLabel> sorted) { Func<DateTime, int> getValue = (d) => 0; switch (scale) { case AxisScale.Years: getValue = (d) => d.Year; break; case AxisScale.Months: getValue = (d) => d.Month; break; case AxisScale.Days: getValue = (d) => d.Day; break; case AxisScale.Hours: getValue = (d) => d.Hour; break; case AxisScale.Minutes: getValue = (d) => d.Minute; break; } int last = -1; foreach (var item in sorted) { var val = getValue(item.Date); if (last == val) { return true; } if (last != val) { last = val; } } return false; } private object GetItem( IEnumerable enumerable, int index) { if (enumerable is IList) { return (enumerable as IList)[index]; } return enumerable .OfType<object>() .Skip(index) .First(); } private enum AxisScale { Years, Months, Days, Hours, Minutes, Unknown } }
Hope this helps!
ok,
My confusion stemmed from you describing that the data is possibly sparse, yet all the points should be rendered as equidistant from eachother?
I'll see if I can show you an example of dynamically adjusting the axis labels.