Pre loader

1

Welcome to the SciChart Forums!

  • Please read our Question Asking Guidelines for how to format a good question
  • Some reputation is required to post answers. Get up-voted to avoid the spam filter!
  • We welcome community answers and upvotes. Every Q&A improves SciChart for everyone

WPF Forums | JavaScript Forums | Android Forums | iOS Forums

8 votes

I managed to come up with a solution which turned out to be a great exercise in what it truly possible with the ChartModifierBase class. The full source for XABCDModifier is attached but I want to highlight a few points for readers. The code snippets below are not all of the class but merely the high points. I’ve documented the attached code really well for clarity.

1. General Requirements
– Quick to create for the user.
– Fully utilize SciChart’s surface calculations for panning, zooming, etc.
– Move with the data series.
– Be placed anywhere on the visible chart.
– Serve as a template for other annotations which I may want to “link” in the future.
– Support dynamic creation.
– Click-driven.
– Point must be editable without breaking the pattern (lines stay connected or “linked”).

2. Solution Overview
– Custom chart modifier inheriting from ChartModifierBase
– Uses double click to activate and deactivate (primarily for testing, needs to be data bound in production so something which won’t interfere with other modifiers).
– Uses a temporary storage of Dictionary<string, AnnotationBase> for easy access.
– Uses the Tag property for distinguishing these annotations from independent annotations created later.
– Uses data binding in code

Enabling/Disabling
There’s definitely nothing Earth-shattering here and this is not how I’d do this in production. Enabling and disabling here is achieved by a simple double click which is very convenient for testing and keeping the modifier free from external dependencies.

    /// <summary>
    /// Called when a Mouse DoubleClick occurs on the parent <see cref="T:SciChart.Charting.Visuals.SciChartSurface" />
    /// </summary>
    /// <remarks>
    /// This overridden method is only used to activate/deactive the modifer for testing purposes.
    /// In a production setting, this modifier will need to be activated via a view model based on appropriate criteria.
    /// </remarks>
    /// <param name="e">Arguments detailing the mouse button operation</param>
    public override void OnModifierDoubleClick(ModifierMouseArgs e)
    {
        base.OnModifierDoubleClick(e);

        if (_isInDrawMode)
        {
            _linkedLines.Clear();
            _isCreationActive = false;
            _currentAnnotation.IsEditable = true;
        }
        // turn on and off via double click
        _isInDrawMode = !_isInDrawMode;
    }

Handling Key Inputs
In my opinion, a complete user experience can’t be achieved without handling key presses so I implemented a few. The escape key was originally used to serve as a short circuit during testing and should probably remove the entire pattern but here it just stops drawing. The rest of the code here is pretty self-explanatory.

    /// <summary>
    /// Called when the KeyDown event is fired for the Master of the current <see cref="P:SciChart.Charting.ChartModifiers.ChartModifierBase.MouseEventGroup" />.
    /// </summary>
    /// <remarks>
    /// Used during testing environment to stop creation without a mouse action.
    /// </remarks>
    /// <param name="e">Arguments detailing the key event</param>
    public override void OnModifierKeyDown(ModifierKeyArgs e)
    {
        if (e.Key == Key.Delete)
        {
            TryRemoveAnnotation();
        }

        if (e.Key == Key.Escape)
        {
            _currentAnnotation.IsEditable = true;
            _isInDrawMode = false;
            _isCreationActive = false;
            _linkedLines.Clear();
            return;
        }

        if (!_isInDrawMode) return;

        if (e.Key != Key.LeftCtrl) return;

        base.OnModifierKeyDown(e);

        _currentAnnotation.IsEditable = true;
        _isInDrawMode = false;
        _isCreationActive = false;

        if (_linkedLines.Count > 1)
        {
        }
    }

Pattern Creation
Here is the meat and potatoes of my solution. I’ll go over the helper methods below. Here’s the full OnModifierMouseDown override. First, I make sure I’m in draw mode which is set in the OnModifierDoubleClick. Once I’m drawing, there are 2 cases with a XABCD pattern. The first is just drawing the simple lines which are XA, AB, BC, and CD. Once those have been drawn, I now have a line count of 5. It’s 5, not 4, because I’ve already started drawing the 5th line but just haven’t place it yet. Once I’m at this point, I don’t want to try to precision click the DB and BX lines since I can easily get the B and X points from previously drawn lines (this is why I store them in _linkedLlines). Therefore, I just create the next line which is BX then start placing and binding the remaining points.

    /// <summary>
    /// Called when a Mouse Button is pressed on the parent <see cref="T:SciChart.Charting.Visuals.SciChartSurface" />
    /// </summary>
    /// <remarks>
    /// This is the primary method for utilizing this modifier.
    /// </remarks>
    /// <param name="e">Arguments detailing the mouse button operation</param>
    public override void OnModifierMouseDown(ModifierMouseArgs e)
    {
        if (!_isInDrawMode) return;

        base.OnModifierMouseDown(e);

        if (_isInDrawMode)
        {
            // handle subsequent clicks on an existing annotation
            if (_isCreationActive)
            {
                CompletePatternLeg(e);
                CreatePatternLeg(e);
                LinkAnnotations(_currentAnnotation, _previousAnnotation);

                if (_linkedLines.Count == 5)
                {
                    CompletePatternLeg(e);
                    CreatePatternLeg(e);
                    LinkAnnotations(_currentAnnotation, _previousAnnotation);

                    _previousAnnotation.X2 = _linkedLines["BC"].X1;
                    _previousAnnotation.Y2 = _linkedLines["BC"].Y1;


                    CompletePatternLeg(e);

                    _currentAnnotation.X1 = _linkedLines["DB"].X2;
                    _currentAnnotation.Y1 = _linkedLines["DB"].Y2;
                    _currentAnnotation.X2 = _linkedLines["XA"].X1;
                    _currentAnnotation.Y2 = _linkedLines["XA"].Y1;

                    // need to bind BX X1Y1 to DB X2Y2
                    LinkAnnotations(_linkedLines["BX"], _linkedLines["DB"]);
                    LinkAnnotations(_linkedLines["BC"], _linkedLines["DB"]);
                    LinkAnnotations(_linkedLines["BX"], _linkedLines["AB"]);
                    LinkAnnotations(_linkedLines["XA"], _linkedLines["BX"]);

                    CreateDataBinding(_linkedLines["BX"], AnnotationBase.X1Property, _linkedLines["BC"], "X1");
                    CreateDataBinding(_linkedLines["BX"], AnnotationBase.Y1Property, _linkedLines["BC"], "Y1");
                    CreateDataBinding(_linkedLines["DB"], AnnotationBase.X2Property, _linkedLines["AB"], "X2");
                    CreateDataBinding(_linkedLines["DB"], AnnotationBase.Y2Property, _linkedLines["AB"], "Y2");

                    _previousAnnotation.IsSelected = false;
                    _currentAnnotation.IsEditable = true;
                    _currentAnnotation.IsSelected = false;
                    _isInDrawMode = false;
                    _isCreationActive = false;

                    _linkedLines.Clear();
                }
            }
            // handle initially creating an annotation
            else
            {
                _isCreationActive = !_isCreationActive;
                CreatePatternLeg(e);
            }
        }
    }

The general creation is

CompletePatternLeg(e); // we've already started drawing so complete that line on mouse click
CreatePatternLeg(e); // now create a new line
LinkAnnotations(_currentAnnotation, _previousAnnotation); // link the just completed line X2Y2 to the newly created X1Y1

Update X2Y2 with OnModifierMouseMove

Of note here is if (_linkedLines.Count == 5) since this allows me to finish the XABCD pattern programmatically instead of trying to precision click. The data binding of the points for the last 2 lines is a little different so I handled those individually.

Helper Methods
These are what make everything “just work”

Adding a new line. Self explanatory but important if you want to see your work.

    /// <summary>
    /// Adds the annotation.
    /// </summary>
    /// <remarks>
    /// The _linkedLines dictionary is used to allow connecting non-consecutive annotations in a group or pattern.
    /// _linkedLines dictionary is cleared after the XABCDModifier is deactivated.
    /// </remarks>
    /// <param name="legName">Name of the leg.</param>
    /// <param name="annotation">The annotation.</param>
    private void AddAnnotation(string legName, AnnotationBase annotation)
    {
        ParentSurface.Annotations.Add(annotation); // required for annotation to be rendered on SciChartSurface
        _linkedLines.Add(legName, annotation); // used internally for advanced/custom annotation linking
    }

Data Binding Line Points
This is by far the most crucial piece of making this work with annotations. This method works for the XA, AB, BC, CD lines then I manually handle the DB and BX lines in the Count == 5 case. This method is best thought of as reading the parameters from left to right. Bind TARGET’s TARGETPROPERTY to SOURCE’s SOURCEPROPERTY. I tried using BindingMode.TwoWay but it didn’t work as expected so below is what did work.

    /// <summary>
    /// Creates the data binding.
    /// </summary>
    /// <remarks>
    /// To prevent annotations from separating, call this function once for each annotation's properties, i.e. line1's X2Y2 and line2's X1Y1.
    /// This will allow either annotation itself, or adorner handles to be selected and still keep the annotations connected.
    /// </remarks>
    /// <param name="target">The target.</param>
    /// <param name="targetProperty">The source property.  This must be a DependencyProperty.</param>
    /// <param name="source">The source.</param>
    /// <param name="sourceProperty">The string representation of the property to be data bound.</param>
    private void CreateDataBinding(AnnotationBase target, DependencyProperty targetProperty, AnnotationBase source, string sourceProperty)
    {
        Binding binding = new Binding();
        binding.Mode = BindingMode.OneWay;
        binding.Source = source;
        binding.Path = new PropertyPath(sourceProperty);
        target.SetBinding(targetProperty, binding);
    }

Creating and completing pattern legs is fairly straightforward but it has some built in “gotchas” that I encountered. When creating an annotation, you must set all coordinate properties ****before**** it may be added to the ParentSurface.Annotations collection. In a slight of hand, I just set both sets of X and Y to the current mouse point position since I’ll just turn right around and update X2Y2 in OnModifierMouseMove. This allow me to create the annotation and receive the real time visual feedback when placing the end of the line.

    private void CompletePatternLeg(ModifierMouseArgs e)
    {
        _previousAnnotation = _currentAnnotation;
        _previousAnnotation.IsEditable = true; // can't be set while placing (interferes with mouse clicks), must be after placed
        _previousAnnotation.IsSelected = false;
    }

    private void CreatePatternLeg(ModifierMouseArgs e)
    {
        _currentAnnotation = CreateNewLine();

        SetAllCoordinates(_currentAnnotation, e.MousePoint);
        AddAnnotation(GetXABCDLegName(), _currentAnnotation);
    }

Creating the line itself is about as simple as it gets. The X/YAxisId must be set though, ask me how I know…
Naming the lines is a simple switch statement that is hard coded.

    /// <summary>
    /// Creates the new line.
    /// </summary>
    /// <remarks>
    /// The Tag property is specifically set so these annotations may be differentiated
    /// from other annotations in <see cref="T:SciChart.Charting.Visuals.SciChartSurface.Annotations"/>.
    /// When utilizing from another class or resource, the Tag property must be explicitly casted to a string.
    /// 
    /// This function could be easily extended to support other annotation types by switching on a DependencyProperty
    /// that specifies the type of annotations this modifier should create.
    /// </remarks>
    /// <returns>AnnotationBase.</returns>
    private AnnotationBase CreateNewLine()
    {
        /*var gb = new LinearGradientBrush();
        gb.MappingMode = BrushMappingMode.RelativeToBoundingBox;
        gb.StartPoint = new Point(0, 0);
        gb.EndPoint = new Point(1, 1);
        gb.GradientStops.Add(new GradientStop(Colors.Blue, 0.0));
        gb.GradientStops.Add(new GradientStop(Colors.LawnGreen, 1.0));*/
        var line = new LineAnnotation
        {
            //Tag = ANNOTATION_IDENTIFIER,
            Tag = GetXABCDLegName(),
            StrokeThickness = 2,
            Stroke = Brushes.LawnGreen,
            XAxisId = ParentSurface.XAxis.Id,
            YAxisId = ParentSurface.XAxis.Id
        };

        line.Selected += OnLineSelected;

        return line;
    }

Hopefully this will help the SciChart community as much as it has helped me. This was an awesome experience and really forced me to understand the SciChart API. Also, a big thanks to @Andrew for helping me avoid going down the rabbit hole of trying to do this with a RenderableSeries since I’m using CategoryDateTimeAxis (CDTA requires a 1:1 point count for any additional series even XyDataSeries).

Cheers,

Jason

End result:
End Result

5 votes

Hi Jose,

Sorry to interrupt Yuriy’s excellent support handling! There is a little known (and not documented) feature which we included to provide users with a workaround in cases like this.

There is a property called SciChartSurface.ViewportManager (expects IViewportManager) which is queried on render to get the X and Y axis VisibleRange.

See below the implementation of DefaultViewportManager:

using System.Diagnostics;

   /// <summary>
    /// The DefaultViewportManager performs a naive calculation for X and Y Axis VisibleRange. 
    /// On each render of the parent SciChartSurface, either autorange to fit the data (depending on the Axis.AutoRange property value), 
    /// or return the original axis range (no change)
    /// </summary>
    public class DefaultViewportManager : ViewportManagerBase
    {
        /// <summary>
        /// Called when the <see cref="IAxis.VisibleRange"/> changes for an axis. Override in derived types to get a notification of this occurring
        /// </summary>
        /// <param name="axis">The <see cref="IAxis"/>instance</param>
        public override void OnVisibleRangeChanged(IAxis axis)
        {
        }

        /// <summary>
        /// Called when the <see cref="ISciChartSurface" /> is rendered.
        /// </summary>
        /// <param name="sciChartSurface">The SciChartSurface instance</param>
        public override void OnParentSurfaceRendered(ISciChartSurface sciChartSurface)
        {            
        }

        /// <summary>
        /// Overridden by derived types, called when the parent <see cref="SciChartSurface" /> requests the XAxis VisibleRange.
        /// The Range returned by this method will be applied to the chart on render
        /// </summary>
        /// <param name="xAxis">The XAxis</param>
        /// <returns>
        /// The new VisibleRange for the XAxis
        /// </returns>
        protected override IRange OnCalculateNewXRange(IAxis xAxis)
        {
            // Calculate the VisibleRange of X Axis, depending on AutoRange property
            if (xAxis.AutoRange)
            {
                var newXRange = xAxis.GetMaximumRange();
                if (newXRange != null && newXRange.IsDefined)
                    return newXRange;
            }

            return xAxis.VisibleRange;
        }

        /// <summary>
        /// Overridden by derived types, called when the parent <see cref="SciChartSurface" /> requests a YAxis VisibleRange.
        /// The Range returned by this method will be applied to the chart on render
        /// </summary>
        /// <param name="yAxis">The YAxis</param>
        /// <param name="renderPassInfo"></param>
        /// <returns>
        /// The new VisibleRange for the YAxis
        /// </returns>
        protected override IRange OnCalculateNewYRange(IAxis yAxis, RenderPassInfo renderPassInfo)
        {
            if (yAxis.AutoRange && renderPassInfo.PointSeries != null && renderPassInfo.RenderableSeries != null)
            {
                var newYRange = yAxis.CalculateYRange(renderPassInfo);
                if (newYRange != null && newYRange.IsDefined)
                {
                    return newYRange;
                }
            }

            return yAxis.VisibleRange;
        }
    }

If you create a class like the above and change the behaviour of OnCalculateNewYRange to return a VisibleRange including your threshold level then you can effectively override the AutoRanging of the chart.

Try this method and let me know how it works

Best regards,
Andrew

5 votes

NOTE: RubberBandXyZoomModifier, ZoomPanModifier, MouseWheelZoomModifier, XAxisDragModifier are not compatible with AutoRange.Always. To use AutoRange.Always as well as manual zooming, you will need to temporarily disable AutoRange as you zoom, e.g. on mouse-down disable AutoRange and on mouse double-click re-enable it.

Two ways you could do this:

  1. Handle MouseDown/MouseDoubleClick on the parent SciChartSurface
  2. Do this by creating a class which inherits RubberBandXyZoomModifier and overriding OnModifierMouseDown, OnModifierMouseDoubleClick

For example

public class RubberBandXyZoomModifierEx : RubberBandXyZoomModifier
{
    /// <summary>
    /// When the user does mouse drag & mouse up, stop AutoRange and perform the zoom
    /// </summary>
    public override void OnModifierMouseUp(ModifierMouseArgs e)
    {
        // Disable AutoRange on all axis!
        foreach (var axis in ParentSurface.YAxes)
        {
            axis.AutoRange = AutoRange.Never;
        }
        foreach (var axis in ParentSurface.XAxes)
        {
            axis.AutoRange = AutoRange.Never;
        }

        // Now do the zoom
        base.OnModifierMouseUp(e);
    }

    /// <summary>
    /// When the user double clicks, re-enable zoom and zoom to extents
    /// </summary>
    public override void OnModifierDoubleClick(ModifierMouseArgs e)
    {
        base.OnModifierDoubleClick(e);

        // Reset AutoRange on all axis
        foreach (var axis in ParentSurface.YAxes)
        {
            axis.AutoRange = AutoRange.Always;
        }
        foreach (var axis in ParentSurface.XAxes)
        {
            axis.AutoRange = AutoRange.Always;
        }

        // Now zoom to extents! 
        ParentSurface.ZoomExtents();
    }
}

Update Oct 2014

We now have an documented workaround in the Knowledgebase showing how to mix Zoom, Pan modifiers with AutoRange. This is the technique we use in the Real Time Ticking Stock Charts example to automatically scroll the latest N points, but allow zooming and panning as well.

Best regards,
Andrew

5 votes

You know I have another idea for you, and it might just work.

Try using our XyzDataSeries. If you bind this to a RenderableSeries (e.g. a line or scatter series) it will by default using the XY values for drawing. The Z values are superfluous.

On Hit-Test you could show the Z-values as well as X and Y.

If you wanted just a time value, then store this in the Z. If however you wanted a more complex object (e.g. Metadata or ViewModel) you could store an index in here and have a separate array or list of ViewModels that you index.

In fact, I seem to remember this being asked before. So we have a sample in our TestSuite which does both storing Z-States in metadata and RolloverLabel Templating. Please see the attached.

If you have any questions please let us know!

Andrew

4 votes

OK, for the archives, in case someone doesn’t have enough reading to do:

So this was not the simplest of issues to solve, though perhaps I made it more complex than it needed to be. The basic requirement was that I had to find a way to get arbitrary metadata into a tooltip based on the results invoked by a viewmodel method, when that metadata had no

I ended up creating a SortedList in which I store my series data (I have a lot of pre-processing to do). DataPoint, and one of its properties, DataPointMetadata, are both structs.

Now, RolloverModifer generates the SeriesInfo DTO, which contains the DataSeriesIndex property — as far as I can tell, the index of the datapoint in SciChart’s underlying collection. This, along with the SeriesName, allow me to uniquely identify a datapoint in very quick time.

The remaining challenge is find a way to look up the value using the viewmodel from inside RolloverModifier.ToolTipLabelTemplate. One option was to use MultiBinding, but I’m not sure that would have worked. I instead decided to use a BindingProxy technique and pass the viewmodel as a staticresource to the converter which is responsible for realizing the data.

<scichart:RolloverModifier
    x:Name="rollover"
    DrawVerticalLine="False"
    ShowAxisLabels="False"
    SourceMode="AllVisibleSeries"
    ShowTooltipOn="MouseRightButtonDown"
    >
    <scichart:RolloverModifier.TooltipLabelTemplate>
        <ControlTemplate>
            <Border x:Name="bdToolTip"
                BorderThickness="1.25"
                Background="#341B0B" Opacity="0.85"
                    >
                <Border.Resources>
                    <Style TargetType="{x:Type TextBlock}">
                        <Setter Property="FontSize" Value="12" />
                        <Setter Property="Margin" Value="3" />
                    </Style>
                </Border.Resources>
                <Border.BorderBrush>
                    <StaticResource ResourceKey="bControlAltBackground"/>
                </Border.BorderBrush>
                <Grid >
                    <Grid.RowDefinitions>
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="{Binding SeriesName}" />
                    <TextBlock Grid.Row="1" Grid.Column="0" Text="X: " />
                    <TextBlock Grid.Row="1" Grid.Column="1"
                        Text="{Binding XValue, Converter={StaticResource convIComparable}, StringFormat=\{0:N3\}}"
                                                                    />

                    <TextBlock Grid.Row="2" Grid.Column="0" Text="Y: " />
                    <TextBlock Grid.Row="2" Grid.Column="1"
                        Text="{Binding YValue, Converter={StaticResource convIComparable}, StringFormat=\{0:N3\}}" />
                    
                    <!-- A UC that shows the specially-retrieved metadata -->
                    <uc:DataPointMetadata Grid.Row="3" Grid.ColumnSpan="2"
                                          DataContext="{Binding
                                                        Converter={StaticResource convSeriesInfoToMetadata},
                                                        ConverterParameter={StaticResource proxy},
                                                        Mode=OneWay}"
                                          />

                </Grid>
            </Border>
        </ControlTemplate>
    </scichart:RolloverModifier.TooltipLabelTemplate>
</scichart:RolloverModifier>
/// <summary>
/// Converts a SeriesInfo Object to a <c>DataPointMetadata</c> struct
/// by invoking a method on the viewmodel. One way only.
/// </summary>
public class SeriesInfoToMetadataConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        //cast to viewmodel from proxy
        var vmodel = (vm.SciChartPlotViewModel)((lib.BindingProxy)parameter).Data;
        
        //cast to SeriesInfo from value
        var si = (Abt.Controls.SciChart.SeriesInfo)value;

        //retrieve metadata using viewmodel; viewmodel will query our own series builder facade to retrive the correct
        //data point object, which will contain the metadata
        Pellion.Notebook.Plotting.DataPoint dp = vmodel.GetDataPoint(si.SeriesName, si.DataSeriesIndex);
        return dp.Metadata; //that was easy!
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); }
}
  • dsantimore answered 10 years ago
  • last active 10 years ago
4 votes

Hi,

Please, try translating the mouse point relative to ModifierSurface before using it. Refer to the following code:

_startPoint = GetPointRelativeTo(e.MousePoint, ModifierSurface);

Thing are done in this way in modifiers code. Also, maybe you need to play around with the ReceiveHandledEvents property (if your modifiers are connected together via MouseEventGroup).

Regarding the second question, we don’t provide such an event, but there is the SciChartSurface.Rendered event, which will be fired after each redraw (initiated either by pan, or zoom, or resize operation). Alternatively, you can subscribe to the AxisBase.VisibleRangeChanged event or extend modifiers classes and introduce your own events for them. So in this case you need to subscribe to the SizeChanged event.

Hope this helps,

Best regards,
Yuriy

4 votes

UPDATE

SciChart v3.0 now supports a VerticalSliceModifier, which provides a draggable Vertical line which shows series values as Rollover style tooltips as you drag the line. You can see an example of it in action here:

Series Vertical Slices Example

4 votes

(I’m fairly new with SciChart myself, so I’m not sure if this is the best way to do it…)
You can look at what I’m building for a start. (Functional, but no comments yet. Hopefully the names are obvious enough)
I’m sure there will be a few extensions missing, but hopefully it’ll be obvious what they do.

When you add an annotation, it will show the textbox and tail pointers at an offset from the referenced data point.
When you zoom in and out and pan around, the annotation components will retain their relative size and offset to the point.
In edit mode, you can drag the text and reference points to new locations.

Here are two of the main extensions I know of:

    public static void AddHandler(this DependencyProperty prop, object component, EventHandler handler)
    {
        DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
        if (dpd != null)
            dpd.AddValueChanged(component, handler);
    }
    public static void AddHandler(this object component, DependencyProperty prop, EventHandler handler)
    {
        DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
        if (dpd != null)
            dpd.AddValueChanged(component, handler);
    }
4 votes

Hello Joerg,

we created an example for you. You only need to add reference on scichart.dll to run this code.

Best regards,
Yuriy

4 votes

Hi there, and thanks for your enquiry!

It just so happens we have a sample for this already in our internal test-suite. Please see the attached.

What it does is append LabelAnnotations for the values on a Histogram plot. The solution is quite simple, it just uses this code to ensure label annotations are placed centred and above columns:

private void HistogramExampleView_Loaded(object sender, System.Windows.RoutedEventArgs e)
{            
    var series = new XyDataSeries<double, double>();
    series.SeriesName = "Histogram";

    var yValues = new double[] { 0.0, 0.1, 0.2, 0.4, 0.8, 1.1, 1.5, 2.4, 4.6, 8.1, 11.7, 14.4, 16.0, 13.7, 10.1, 6.4, 3.5, 2.5, 1.4, 0.4, 0.1, 0.0, 0.0 };

    for (int i = 0; i < yValues.Length; i++ )
    {
        // DataSeries for appending data
        series.Append(i, yValues[i]);   
 
        // Annotations for text labels
        var labelAnnotation = new TextAnnotation()
                                    {
                                        FontSize = 10,
                                        X1 = i,
                                        Y1 = yValues[i],
                                        Text = yValues[i].ToString(),
                                        Foreground = new SolidColorBrush(Color.FromArgb(0xFF,0x33,0x33,0x33)),
                                        FontWeight = FontWeights.Bold,
                                        VerticalAnchorPoint = VerticalAnchorPoint.Bottom,
                                        HorizontalAnchorPoint = HorizontalAnchorPoint.Center
                                    };

        this.sciChart.Annotations.Add(labelAnnotation);
    }

    columnSeries.DataSeries = series;
}

If you wanted to do something more complex, maybe dynamically place annotations based on changing data, then you could look at this post, which describes an architecture for managing and keeping track of annotations using custom chartmodifiers.

Andrew

4 votes

Hi there,

Please see our KB article on How to Add Mouse Interaction to SciChart, the section on ChartModifier Precedence and Handled mouse events.

ChartModifier Precedence (Handled Events)

ChartModifiers obey a precedence, rather like WPF RoutedEvents. If you have a number of modifiers in a SciChartSurface, then the first modifier that handles an event marks it as e.Handled. Subsequent modifiers will not receive the event.

For instance, consider the following code:


    
        
        
             
             
             
             
        
    

If you drag the chart, then the series will not be selected too, because the RubberBandXyZoomModiifer marks MouseUp as Handled. The SeriesSelectionModifier relies on MouseUp to select. If the event is handled then it will not select.

So far this is all intuitive and good. But what about this case?


    
        
        
             
             
             
             
        
    

Working around event handling with ReceiveHandledEvents

The solution to the above problem is to set ReceiveHandledEvents=True on a modifier. This way the modifier will receive all events, even those marked as handled. Try the following code for instance:


    
        
        
             
             
             
             
        
    

Be careful with this flag though as it could enable event handling where it should not occur.

4 votes

SciChart v3.0 now allows detection of whether VisibleRangeChanged was fired during animation or not. Try this code below:

public void Foo()
{
    var axis = new NumericAxis();

    axis.VisibleRangeChanged += (s, e) =>
        {
            if (e.IsAnimating)
            {
                // VisibleRangeChanged occurred during animation                        
            }
            else
            {
                // VisibleRangeChanged did not occur during animation
            }
        };
}
4 votes

Hi Manish,

I’ve just investigated this now – you’re absolutely right! The above code doesn’t work on the axes. Here. Try this – its a custom modifier I’ve developed which demonstrates hit testing of the YAxis and XAxis.

This code lets you detect click on axis.

public class HitTestingModifier : ChartModifierBase
{
    public override void OnModifierMouseDown(ModifierMouseArgs e)
    {
        bool isOnChart = IsPointWithinBounds(e.MousePoint, ModifierSurface);
        bool isOnYAxis = IsPointWithinBounds(e.MousePoint, YAxis);
        bool isOnXAxis = IsPointWithinBounds(e.MousePoint, XAxis);

        MessageBox.Show(string.Format("Clicked YAxis? {0}\nClicked XAxis? {1}\nClicked Chart? {2}", isOnYAxis,
                                        isOnXAxis, isOnChart));

        base.OnModifierMouseDown(e);
    }

    public bool IsPointWithinBounds(Point point, IHitTestable element)
    {
        var tPoint = ParentSurface.RootGrid.TranslatePoint(point, element);

        bool withinBounds = (tPoint.X <= (element as FrameworkElement).ActualWidth && tPoint.X >= 0)
                            && (tPoint.Y <= (element as FrameworkElement).ActualHeight && tPoint.Y >= 0);

        return withinBounds;
    }
}

If you click in the XAxis, YAxis or chart region you will get a message box showing what element has been clicked.

Example attached

Thanks!

4 votes

Thanks to Andrew, setting the ResamplingMode to Auto seems to have fixed the problem

  • kewur answered 9 years ago
4 votes

Hello there,

I’m investigating this now.

I can confirm if I run the application on my pc, while it doesn’t lock up by 16,000 points, it is certainly slower. What hardware do you have out of interest? It shouldn’t make much difference (unless its really slow). Mine is quad-core i7. 2.4GHz, so nothing stellar, but no sluggard either.

(BTW I’m really impressed with the SciChart application you’ve written in such a short period of time as in the trial. Nice application & really impressive job!)

Now, I ran your test application through DotTrace. The results are attached.

They make interesting reading! Basically the SciChartOverview is responsible for 40% of the time spent on the main thread. Particularly where the overview control is zooming itself to extents on each point added (on each redraw). We haven’t really optimized the overview for performance as it never raised itself as a bottleneck – it has now!

So, my first piece of advice is, remove the overview

The next interesting thing is that the axis drawing takes 10% of the time. In particular, Axis tick-drawing takes 6.4%. Remember if we remove overview all these figures double, so the tick drawing alone is quite significant.

My second recommendation is to Set AxisBase.DrawMinorTicks = false on all XAxes and YAxes

This will reduce the number of lines these are adding to the screen per frame. Axis ticks and labels are drawn using WPF canvases which are far slower than bitmap renderered series, so we want to minimise these if at all possible. If you can handle it setting AxisBase.DrawMinorGridLines = false will further improve performance since it will reduce the number of elements on the screen, however, there is only one set of gridlines for N axes and there are N sets of minor ticks, so minor ticks are the low hanging fruit here.

My third recommendation is to set SciChartSurface.RenderPriority = RenderPriority.Low

What does this do? It puts the priority of the render thread below the priority of mouse and keyboard input. This is really important as once the UI thread approaches saturation you will lose keyboard and mouse interactivity. This will really annoy your users and give you the impression that the application is ‘locked up’. By using priority low for rendering you give control back to the message loop to process mouse and key events and make your application a lot happier. The trade off is the chart rendering may appear more stuttery but it is because redraws are postponed slightly while the WPF message loop is flushed, resulting in a more fluid application experience.

If you want you can go one further and set RenderPriority.Manual. Then you are responsible for calling SciChartSurface.InvalidateElement() after appending points. For instance, you could set a timer and call InvalidateElement() every 1/10th of a second, rather than for each point appended.

My final recommendation is to throttle data appended to the chart, using the DataSeries.Append(IEnumerable<TX> xValues, IEnumerable<TY> yValues) API

The chart dataseries already do a good job of throttling data. For instance, there is not necessarily 1 redraw for each point appended. However, on each point appended some calculations are performed, such as Min Max calculations of the DataSeries, and checking internal state of the chart before pushing a redraw message onto a stack for the renderer thread to pick up. This is a small overhead but if you are calling DataSeries.Append in a tight loop it will soon add up. You can buffer or throttle appends but using the DataSeries.Append(IEnumerable<TX> xValues, IEnumerable<TY> yValues) method. Passing in arrays here of even size 10 or 100 points at a time can significantly improve throughput.


I’ve just tried the above (except for buffering Appends since that would require bigger changes to the code than I can do here) and I’m seeing task manager report a more healthy 7% CPU at ~16k points rather than the 29% I was seeing before. Can you try these and see if you get the same?

Regarding the overview, we’ll work on that. I suggest leaving it out for now and replacing with a scrollbar, calculating the offset based on XAxis.VisibleRange vs. the known DataSeries.XValues

Best regards,
Andrew

4 votes

Update June 2014: The Short Answer

We’ve now integrated the X and Y Values to the Rollovermodifier natively, and this is demonstrated in our examples. Please see the Realtime Cursors Example which demonstrates a Rollover with X and Y Values in the Legend area.

The Long Answer

The way it works is like this. The RolloverModifier and LegendModifier are simply data-sources, which give you an ObservableCollection<SeriesInfo> to bind to in XAML.

The Series Info classes are defined in the API documentation here:

Each Series has its own SeriesInfo type. All of them inherit from SeriesInfo, which is the base type. So given you are binding to a collection of SeriesInfo when you use the RolloverModifier or LegendModifier, it becomes possible to expose almost any info about the underlying RenderableSeries or DataSeries.

As a starting point please see our RolloverModifier demo, in particular the source code where we create the ItemsControl to consume SeriesInfo objects:

<!-- Binds to SeriesInfo, outputs Y-Values only -->
<!-- By modifying this and using knowledge of SeriesInfo class definition above, -->
<!-- you can theoretically do anything! --> 
<Border Grid.Row="1" Margin="23,23" HorizontalAlignment="Left" VerticalAlignment="Top" Background="#77FFFFFF" BorderBrush="#55000000" BorderThickness="2" Padding="5">
    <ItemsControl DataContext="{Binding ElementName=rolloverModifier}" ItemsSource="{Binding RolloverData.SeriesInfo}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Column="0"
                               Margin="3,3,20,3"
                               FontSize="13"
                               FontWeight="Bold"
                               Foreground="{Binding SeriesColor,
                                                    Converter={StaticResource ColorToBrushConverter}}"
                               Text="{Binding SeriesName}" />
                    <TextBlock Grid.Column="1"
                               Margin="3,3,3,3"
                               FontSize="13"
                               FontWeight="Bold"
                               Foreground="{Binding SeriesColor,
                                                    Converter={StaticResource ColorToBrushConverter}}"
                               Text="{Binding Value}" />
                </Grid>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Border>

Note that when you bind to XValue / YValue which are of type IComparable, you will need a converter or a StringFormat to convert from IComparable to the display value. Yes believe it or not, in XAML if you bind an IComparable which is actually a double to a TextBlock it won’t display! Please use an IComparable converter if you get blank values when binding to these properties.

In the RealTimeCursors example we see this code to convert the XValue, YValue to doubles:

<!-- When binding to XValue, YValue of type IComparable, StringFormat is mandatory due to a -->
<!-- XAML bug that cannot convert IComparable to text, even though underlying type is double -->
<StackPanel Orientation="Horizontal" Grid.Column="2">
    <TextBlock Text="X: " Style="{StaticResource tbStyle}"/>
    <TextBlock Text="{Binding XValue, StringFormat=\{0:0.00\}}" Style="{StaticResource tbStyle}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="3">
    <TextBlock Text="Y: " Margin="3" Style="{StaticResource tbStyle}"/>
    <TextBlock Text="{Binding YValue, StringFormat=\{0:0.00\}}" Style="{StaticResource tbStyle}"/>
</StackPanel>

Advanced SeriesInfo Binding

Finally, many people ask us how to bind to a specific property on the RenderableSeries or DataSeries. Well, the SeriesInfo exposes the RenderableSeries too so its possible to expose any property from the RenderableSeries. Don’t forget RenderableSeries.DataSeries also allows access from SeriesInfo right back to the original DataSeries.

  • Andrew
4 votes
In reply to: Documentation

Hello Catalin,

Yes there is. All of our documentation is now online. Please see this related question: Where is the Documentation.

We also have a demo on Xaml styling in the examples suite, Please see our WPF Chart Xaml Styling Example

4 votes

I am considering applying server-side licensing for my javerScript application.

In the document below, there is a phrase “Our server-side licensing component is written in C++.”
(https://support.scichart.com/index.php?/Knowledgebase/Article/View/17256/42/)

However, there is only asp.net sample code on the provided github.
(https://github.com/ABTSoftware/SciChart.JS.Examples/tree/master/Sandbox/demo-dotnet-server-licensing)

I wonder if there is a sample code implemented in C++ for server-side licensing.

Can you provide c++ sample code?
Also, are there any examples to run on Ubuntu?

4 votes

Hello Joerg,

Axis Label styling was introduced in SciChart v3.0. You can now set a Style for a label including margins, LayoutTransforms etc…

Please see the answer on Styling Axis Labels for more information.

Best regards,
Yuriy

4 votes

Sure, you can either use a TextAnnotation for an axis label on top of the axis. Just give the position for the Y axis, 0, yaxisMax + font height. You can also use regular Labels on top of the graph if you want to do that. I think you can also make your own custom NumericAxis to be able to do this.

The second part is easier, just make two textboxes and bind them to a property called YAxisMin YAxisMax, XAxisMin, XAxisMax. You can also bind them to your axis like this.

xAxis.SetBinding(NumericAxis.MinRange, new Binding(“XAxisMin”));

good luck.

  • kewur answered 9 years ago
Showing 1 - 20 of 6k results

Try SciChart Today

Start a trial and discover why we are the choice
of demanding developers worldwide

Start TrialCase Studies