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?
So if there is a large gap in time between values in the chart, you would want there to be a large space, and an interpolated line from the previous to the next point?
The CategoryDateTimeXAxis on the xamDataChart fulfills these needs but its axis labels do not natively support some of the advanced scenarios you propose. It may be possible to augment their abilities with application code to meet your requirement, though, and you could make a feature request.
I will experiment with your request and get back to you.
-Graham
Graham Murray"]So if there is a large gap in time between values in the chart, you would want there to be a large space, and an interpolated line from the previous to the next point?
If I understand you correctly that is not what I'm asking. My request concerns the X axis labels only, not the data points and how they're connected.
Or should all data points be evenly spaced from each other?
The data point position should not be influenced. The scale of the X axis doesn't change, only how the x axis labels are formatted, and which x axis labels are shown.
It is basically a way to optimally use space beneath the x axis.
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.
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!
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
Thanks for the example Rob, your example works as advertised. Unfortunately, CategoryXAxis requires all series on the chart to have all x-values "aligned", so if I have two series that do not have completely matching times for the x-axis data, the second series will not be rendered (see http://es.infragistics.com/community/forums/p/38082/220246.aspx). I tried to drop in a CategoryDateTimeXAxis in the example since it allows "non-aligned" x-values but that doesn't work - for example, simply changing this:
<igChart:CategoryXAxis x:Name="xAxis" ItemsSource="{StaticResource data}">
to this:
<igChart:CategoryDateTimeXAxis x:Name="xAxis" ItemsSource="{StaticResource data}" DateTimeMemberPath="Date">
does not work. Any suggestions on how to alter your example for this type of X-Axis?
Since the time Graham posted that code we have made some changes to how axis are rendered so that is the likely reason why the code no longer works. I updated the code to work with 15.1 now. Check out my attached sample.
I am using v15.1 and having the same issue as Paril in the comment above. Any ideas as to why DataLabel's aprent is NULL?
I am attempting to run sample using xamDataChart v11.2 but causes an exception at runtime. The "DateLabel" template Parent is null and hence the GetSortedLabels function cannot be executed.
Any ideas anybody?
Hmm...
I don't seem to be experiencing this. Is there anything else you can tell me to help me reproduce the problem?