This project is read-only.

Cursor or Slider on line charts

Dec 29, 2009 at 4:02 PM

Hi,

I have a need to have a movable cursor or a slider on line charts. The way it should work is as follows:

1. Slider/Cursor should be parallel to X-axis i.e. horizontal axis

2. When user moves this slider, corresponding/intersecting Y values on all graphs show up in labels just like a stock ticker chart.

3. Slider movement can be discrete i.e. there is no need for interpolation. This is like fixing the resolution to a predefined value depending on the x-axis value increments.

 

My question is, is there such a feature already available in D3 or is it planned?

If not, what will it take to implement it in D3?

 

Thanks,

Faisal

 

Jan 4, 2010 at 10:57 PM

Hello,

I have a similar requirement: draggable markers (along the X axis) that influence the displayed data. How is it possible to add custom interactive elements to the plot?

Best regards!

BB

Apr 29, 2010 at 9:05 AM

Did you have any luck with this? I would also like to implement a draggable cursor.

Apr 29, 2010 at 5:40 PM

No luck yet. There are built-in cursors/markers but those do not work very well. They jump around and sometimes even the axes jump too.

May be one of the developers can reply and help out.

Apr 30, 2010 at 8:19 AM

sure it's not the right way but can't you like use a cursor coordinated graph, and bind the x and y mapping to the label which is added in the lower panel, so when the user moves the mouse on the chart your label changes as well?

Apr 30, 2010 at 8:44 AM

It's more about not following the mouse cursor. But having a vertical line on the chart that you can move as and when you need to.

Apr 30, 2010 at 9:19 AM

are you guys trying to do something like http://www.google.com/finance?q=C

Apr 30, 2010 at 9:24 AM

I can't speak for others but I my case all that's required is the ability to add vertical cursor line. It can be clicked on and then dragged left or right using the mouse.

A bit like: http://i906.photobucket.com/albums/ac266/McBainUK/MultipleCursors.jpg

I'm currently looking into custom control templates for the D3 DraggablePoint type.

Apr 30, 2010 at 9:30 AM

This is a better example, it shows exactly what I require from the D3 chart.

http://www.syncfusion.com/content/en-US/Products/screenshots/aspnet/img/InteractiveCursor_larger.png

Apr 30, 2010 at 9:47 AM

Hi,

I think the thing you want can be created from VerticalLine and DraggablePoint, with line's x coordinate bound to point's position.X. You really can make a try to create a custom template for draggable point so that there would be no circle with point in its center, but a line or smth like that.

Sorry, now I cannot help you more as I really do not have enough free time.

BTW, you can add an issue to our issue tracker if you think that such type of control you need cannot be composed from line and draggable point, but requires some extra code and worth to exist in D3 as separate chart.

Best regards,

Mikhail.

Apr 30, 2010 at 9:55 AM
Thecentury wrote:

I think the thing you want can be created from VerticalLine and DraggablePoint, with line's x coordinate bound to point's position.X. You really can make a try to create a custom template for draggable point so that there would be no circle with point in its center, but a line or smth like that.

I came to the same conclusion. Merging the line with a draggable point and using a custom template looks like it's working. Thanks Mikhail.

Apr 30, 2010 at 4:29 PM

I'm having trouble creating a template for the DraggablePoint where it draws a vertical line the entire height of the chart. It needs to be like this so you can click and drag on any part of the line, not just the draggable point area. Any thoughts?

Apr 30, 2010 at 8:01 PM

Here is what I did to solve this same problem. I basically copied the CursorCoordinateGraph control and modified it. Then, you just host the new control inside the ChartPlotter like normal.

This is by no means complete, but you could tailor it to your needs. Let me know how it goes.

XAML:

<d3:ContentGraph x:Class="LineCursorGraph"
    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"
    xmlns:local="clr-namespace:Controls"
    IsHitTestVisible="false" Panel.ZIndex="1">
    <d3:ContentGraph.Resources>
        <Style x:Key="outerBorderStyle" TargetType="{x:Type Rectangle}" >
            <Setter Property="RadiusX" Value="10"/>
            <Setter Property="RadiusY" Value="10"/>
            <Setter Property="Stroke" Value="LightGray"/>
            <Setter Property="StrokeThickness" Value="1"/>
            <Setter Property="Fill" Value="#88FFFFFF"/>
        </Style>

        <Style x:Key="innerBorderStyle" TargetType="{x:Type Border}">
            <Setter Property="CornerRadius" Value="4"/>
            <!--<Setter Property="Background" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>-->
            <Setter Property="Margin" Value="8,4,8,4"/>
        </Style>
        
        <Style x:Key="textStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="2,1,2,1"/>
            <Setter Property="Foreground" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>
        </Style>

        <Style x:Key="lineStyle" TargetType="{x:Type Line}">
            <Setter Property="Stroke" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>
            <Setter Property="StrokeThickness" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStrokeThickness}"/>
            <Setter Property="StrokeDashArray" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStrokeDashArray}"/>
            <Setter Property="IsHitTestVisible" Value="true"/>
        </Style>
    </d3:ContentGraph.Resources>
    <Canvas Name="content" Cursor="None" Background="Transparent" IsHitTestVisible="true">
        <Line Name="horizLine" Style="{StaticResource lineStyle}"/>
        <Line Name="vertLine" Style="{StaticResource lineStyle}"/>

        <Grid Name="horizGrid" Canvas.Top="10">
            <!--<Rectangle Style="{StaticResource outerBorderStyle}"/>-->
            <Border Style="{StaticResource innerBorderStyle}">
                <TextBlock Name="horizTextBlock" Style="{StaticResource textStyle}"/>
            </Border>
        </Grid>

        <Grid Name="vertGrid" Canvas.Left="5" Visibility="Collapsed">
            <Rectangle Style="{StaticResource outerBorderStyle}"/>
            <Border Style="{StaticResource innerBorderStyle}">
                <TextBlock Name="vertTextBlock" Style="{StaticResource textStyle}"/>
            </Border>
        </Grid>
    </Canvas>
</d3:ContentGraph>

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Microsoft.Research.DynamicDataDisplay.Charts;
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Research.DynamicDataDisplay;

namespace Controls
{
    /// <summary>
    /// Interaction logic for LineCursorGraph.xaml
    /// </summary>
    public partial class LineCursorGraph : ContentGraph
    {
        #region Enums
        public enum CursorType
        {
            CursorA,
            CursorB,
            Waveform,
            Slicer
        }

        public enum LineCursorOrientation
        {
            Vertical,
            Horizontal
        } 
        #endregion

        #region Dependency Properties
        #region CursorType
        public static readonly DependencyProperty CursorTypeProperty = DependencyProperty.Register("CursorType",
            typeof(CursorType), typeof(LineCursorGraph),
            new PropertyMetadata(CursorType.CursorA, OnCursorTypeChanged));

        public CursorType Type
        {
            get { return (CursorType)GetValue(CursorTypeProperty); }
            set { SetValue(CursorTypeProperty, value); }
        }

        private static void OnCursorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LineCursorGraph line = (LineCursorGraph)d;
            line.OnCursorTypeChanged();
        }

        protected virtual void OnCursorTypeChanged()
        {
            switch (Type)
            {
                case CursorType.CursorA:
                    //_parentCursor = Cursors.SizeNS;
                    LineStroke = Brushes.White;
                    Orientation = LineCursorOrientation.Vertical;
                    Canvas.SetTop(horizGrid, 10);
                    break;
                default:
                case CursorType.CursorB:
                    //_parentCursor = Cursors.SizeWE;
                    LineStroke = Brushes.Yellow;
                    Orientation = LineCursorOrientation.Vertical;
                    Canvas.SetTop(horizGrid, 50);
                    break;
                case CursorType.Waveform:
                    LineStroke = Brushes.LimeGreen;
                    Orientation = LineCursorOrientation.Horizontal;
                    break;
                case CursorType.Slicer:
                    LineStroke = Brushes.Red;
                    Orientation = LineCursorOrientation.Vertical;
                    break;
            }

            UpdateUIRepresentation();
        }
        #endregion

        #region Orientation
        public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation",
            typeof(LineCursorOrientation), typeof(LineCursorGraph),
            new PropertyMetadata(LineCursorOrientation.Vertical, OnOrientationChanged));

        public LineCursorOrientation Orientation
        {
            get { return (LineCursorOrientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LineCursorGraph line = (LineCursorGraph)d;
            line.OnOrientationChanged();
        }

        protected virtual void OnOrientationChanged()
        {
            switch (Orientation)
            {
                case LineCursorOrientation.Horizontal:
                    _parentCursor = Cursors.SizeNS;
                    break;
                default:
                case LineCursorOrientation.Vertical:
                    _parentCursor = Cursors.SizeWE;
                    break;
            }

            UpdateUIRepresentation();
        }
        #endregion

        #region XValue
        /// <summary>
        /// Identifies Value dependency property.
        /// </summary>
        public static readonly DependencyProperty XValueProperty =
            DependencyProperty.Register(
              "XValue",
              typeof(double),
              typeof(LineCursorGraph),
              new PropertyMetadata(
                  0.0, OnXValueChanged));


        /// <summary>
        /// Gets or sets the value in data coordinates
        /// </summary>
        public double XValue
        {
            get { return (double)GetValue(XValueProperty); }
            set { SetValue(XValueProperty, value); }
        }

        private static void OnXValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LineCursorGraph line = (LineCursorGraph)d;
            line.OnXValueChanged();
        }

        protected virtual void OnXValueChanged()
        {
            UpdateUIRepresentation();

            if (SyncValue) // if this is an initial value set, then the other cursors should be synchronized
            {
                RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
                SyncValue = false;
            }
        }
        #endregion 

        #region YValue
        /// <summary>
        /// Identifies Value dependency property.
        /// </summary>
        public static readonly DependencyProperty YValueProperty =
            DependencyProperty.Register(
              "YValue",
              typeof(double),
              typeof(LineCursorGraph),
              new PropertyMetadata(
                  0.0, OnYValueChanged));


        /// <summary>
        /// Gets or sets the value in data coordinates
        /// </summary>
        public double YValue
        {
            get { return (double)GetValue(YValueProperty); }
            set { SetValue(YValueProperty, value); }
        }

        private static void OnYValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LineCursorGraph line = (LineCursorGraph)d;
            line.OnYValueChanged();
        }

        protected virtual void OnYValueChanged()
        {
            UpdateUIRepresentation();

            //if (SyncValue) // if this is an initial value set, then the other cursors should be synchronized
            //{
            //    RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
            //    SyncValue = false;
            //}
        }
        #endregion 
        #endregion

        #region Public Properties
        private string customXFormat = null;
        /// <summary>
        /// Gets or sets the custom format string of x label.
        /// </summary>
        /// <value>The custom X format.</value>
        public string CustomXFormat
        {
            get { return customXFormat; }
            set
            {
                if (customXFormat != value)
                {
                    customXFormat = value;
                    UpdateUIRepresentation();
                }
            }
        }

        private string customYFormat = null;
        /// <summary>
        /// Gets or sets the custom format string of y label.
        /// </summary>
        /// <value>The custom Y format.</value>
        public string CustomYFormat
        {
            get { return customYFormat; }
            set
            {
                if (customYFormat != value)
                {
                    customYFormat = value;
                    UpdateUIRepresentation();
                }
            }
        }

        private bool showHorizontalLine = true;
        /// <summary>
        /// Gets or sets a value indicating whether to show horizontal line.
        /// </summary>
        /// <value><c>true</c> if horizontal line is shown; otherwise, <c>false</c>.</value>
        public bool ShowHorizontalLine
        {
            get { return showHorizontalLine; }
            set
            {
                if (showHorizontalLine != value)
                {
                    showHorizontalLine = value;
                    UpdateVisibility();
                }
            }
        }

        private bool showVerticalLine = true;
        /// <summary>
        /// Gets or sets a value indicating whether to show vertical line.
        /// </summary>
        /// <value><c>true</c> if vertical line is shown; otherwise, <c>false</c>.</value>
        public bool ShowVerticalLine
        {
            get { return showVerticalLine; }
            set
            {
                if (showVerticalLine != value)
                {
                    showVerticalLine = value;
                    UpdateVisibility();
                }
            }
        }

        private bool showCursorText = true;
        /// <summary>
        /// Gets or sets a value indicating whether to display the cursor text.
        /// </summary>
        public bool ShowCursorText
        {
            get { return showCursorText; }
            set
            {
                showCursorText = value;
                if (showCursorText)
                    horizGrid.Visibility = Visibility.Visible;
                else
                    horizGrid.Visibility = Visibility.Collapsed;
            }

        }

        private Workspace _workspace;
        /// <summary>
        /// Gets or sets the Workspace that this control is associated with.
        /// </summary>
        public Workspace Workspace
        {
            get { return _workspace; }
            set
            {
                _workspace = value;
            }
        }

        public bool SyncValue { get; set; }

        private double _customXValue;

        public double CustomXValue
        {
            get { return _customXValue; }
            set
            {
                _customXValue = value;
            }
        }

        private double _maxXValue;

        public double MaxXValue
        {
            get { return _maxXValue; }
            set
            {
                _maxXValue = value;
            }
        }

        private double _minXValue;

        public double MinXValue
        {
            get { return _minXValue; }
            set
            {
                _minXValue = value;
            }
        }
        #endregion

        #region Private Fields
        FrameworkElement _parent;

        Vector blockShift = new Vector(3, 3);

        bool _drag = false;

        Cursor _parentCursor = Cursors.SizeWE; 
        #endregion

        public static RoutedEvent CursorMovedEvent = EventManager.RegisterRoutedEvent("CursorMoved",
            RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(LineCursorGraph));

        public LineCursorGraph()
        {
            InitializeComponent();

            EventManager.RegisterClassHandler(typeof(LineCursorGraph), CursorMovedEvent, 
                new RoutedEventHandler(CursorMovedEventHandler));

            // default settings
            Orientation = LineCursorOrientation.Vertical;
            LineStroke = Brushes.White;
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
        }

        protected override void OnPlotterAttached()
        {
            //base.OnPlotterAttached();

            _parent = (FrameworkElement)Parent;

            _parent.MouseMove += new MouseEventHandler(parent_MouseMove);
            _parent.MouseEnter += new MouseEventHandler(parent_MouseEnter);
            _parent.MouseLeave += new MouseEventHandler(parent_MouseLeave);
            _parent.MouseLeftButtonDown += new MouseButtonEventHandler(parent_MouseLeftButtonDown);
            _parent.MouseLeftButtonUp += new MouseButtonEventHandler(parent_MouseLeftButtonUp);

            UpdateUIRepresentation();
        }

        void parent_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _drag = false;
            _parent.Cursor = Cursors.Cross;
            _parent.ReleaseMouseCapture();
        }

        void parent_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (IsMouseNearLine(e.GetPosition(this)))
            {
                _drag = true;
                _parent.CaptureMouse();
            }
        }

        void parent_MouseLeave(object sender, MouseEventArgs e)
        {
            _parent.ReleaseMouseCapture();
        }

        void parent_MouseEnter(object sender, MouseEventArgs e)
        {
            
        }

        void parent_MouseMove(object sender, MouseEventArgs e)
        {
            if (IsMouseNearLine(e.GetPosition(this)))
            {
                //switch (Orientation)
                //{
                //    case LineCursorOrientation.Horizontal:
                //        _parent.Cursor = Cursors.SizeNS;
                //        break;
                //    default:
                //    case LineCursorOrientation.Vertical:
                //        _parent.Cursor = Cursors.SizeWE;
                //        break;
                //}
                _parent.Cursor = _parentCursor;
                e.Handled = true;
            }
            else
            {
                _parent.Cursor = Cursors.Cross;
            }

            if (_drag)
            {
                double value = 0;
                if (Orientation == LineCursorOrientation.Vertical)
                {
                    value = e.GetPosition(Plotter2D.CentralGrid).ScreenToData(Plotter2D.Viewport.Transform).X;

                    if (value > _minXValue && value < _maxXValue)
                        XValue = value;
                }
                else
                    XValue = e.GetPosition(Plotter2D.CentralGrid).ScreenToData(Plotter2D.Viewport.Transform).Y;

                RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
            }
        }

        bool IsMouseNearLine(Point mousePos)
        {
            Point dataPoint = mousePos.ScreenToData(Plotter2D.Viewport.Transform);

            return (dataPoint.X >= XValue - 10 && dataPoint.X <= XValue + 10);
        }

        void CursorMovedEventHandler(object sender, RoutedEventArgs e)
        {
            LineCursorGraph cursor = (LineCursorGraph)sender;

            var obj = LogicalTreeHelper.GetParent(_parent);

            if (this != cursor && this.Type == cursor.Type && _workspace == cursor.Workspace)
            {
                XValue = cursor.XValue;
                //e.Handled = true;
                
            }
        }

        protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e)
        {
            UpdateUIRepresentation();
        }

        protected override void OnPlotterDetaching()
        {
            //base.OnPlotterDetaching();
        }



        protected void UpdateUIRepresentation()
        {
            if (Plotter2D == null) return;

            var transform = Plotter2D.Viewport.Transform;
            DataRect visible = Plotter2D.Viewport.Visible;
            Rect output = Plotter2D.Viewport.Output;

            Point dataPosition = new Point(XValue, 0);
            Point actualPosition = dataPosition.DataToScreen(transform);

            //horizLine.X1 = output.Left;
            //horizLine.X2 = output.Right;
            //horizLine.Y1 = position.Y;
            //horizLine.Y2 = position.Y;

            vertLine.X1 = actualPosition.X;
            vertLine.X2 = actualPosition.X;
            vertLine.Y1 = output.Top;
            vertLine.Y2 = output.Bottom;

            if(customXFormat != String.Empty && customXFormat != null)
                horizTextBlock.Text = String.Format(customXFormat, _customXValue, YValue);
            else
                horizTextBlock.Text = String.Format("({0:F0} cts, {1:F3} V)", GetRoundedValue(visible.XMin, visible.XMax, XValue), YValue);

            double width = horizGrid.ActualWidth;
            double x = actualPosition.X + blockShift.X;
            if (x + width > output.Right)
            {
                x = actualPosition.X - blockShift.X - width;
            }
            Canvas.SetLeft(horizGrid, x);

            //Point p1 = new Point(Value, Plotter.Viewport.Visible.YMin).DataToScreen(transform);
            //Point p2 = new Point(Value, Plotter.Viewport.Visible.YMax).DataToScreen(transform);

            //LineGeometry.StartPoint = p1;
            //LineGeometry.EndPoint = p2;


        }

        private string GetRoundedValue(double min, double max, double value)
        {
            double roundedValue = value;
            //var log = RoundingHelper.GetDifferenceLog(min, max);
            var log = (int)Math.Round(Math.Log10(Math.Abs(max - min)));
            string format = "G3";
            double diff = Math.Abs(max - min);
            if (1E3 < diff && diff < 1E6)
            {
                format = "F0";
            }
            if (log < 0)
                format = "G" + (-log + 2).ToString();

            return roundedValue.ToString(format);
        }

        private void UpdateVisibility()
        {
            horizLine.Visibility = vertGrid.Visibility = GetHorizontalVisibility();
            vertLine.Visibility = horizGrid.Visibility = GetVerticalVisibility();
        }

        private Visibility GetHorizontalVisibility()
        {
            return showHorizontalLine ? Visibility.Visible : Visibility.Hidden;
        }

        private Visibility GetVerticalVisibility()
        {
            return showVerticalLine ? Visibility.Visible : Visibility.Hidden;
        }

        #region LineStroke property

        public Brush LineStroke
        {
            get { return (Brush)GetValue(LineStrokeProperty); }
            set { SetValue(LineStrokeProperty, value); }
        }

        public static readonly DependencyProperty LineStrokeProperty = DependencyProperty.Register(
          "LineStroke",
          typeof(Brush),
          typeof(LineCursorGraph),
          new PropertyMetadata(new SolidColorBrush(Color.FromArgb(170, 86, 86, 86))));

        #endregion

        #region LineStrokeThickness property

        public double LineStrokeThickness
        {
            get { return (double)GetValue(LineStrokeThicknessProperty); }
            set { SetValue(LineStrokeThicknessProperty, value); }
        }

        public static readonly DependencyProperty LineStrokeThicknessProperty = DependencyProperty.Register(
          "LineStrokeThickness",
          typeof(double),
          typeof(LineCursorGraph),
          new PropertyMetadata(2.0));

        #endregion

        #region LineStrokeDashArray property

        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public DoubleCollection LineStrokeDashArray
        {
            get { return (DoubleCollection)GetValue(LineStrokeDashArrayProperty); }
            set { SetValue(LineStrokeDashArrayProperty, value); }
        }

        public static readonly DependencyProperty LineStrokeDashArrayProperty = DependencyProperty.Register(
          "LineStrokeDashArray",
          typeof(DoubleCollection),
          typeof(LineCursorGraph),
          new FrameworkPropertyMetadata(DoubleCollectionHelper.Create(3, 3)));

        #endregion
    }
}

 

 

May 19, 2010 at 10:35 AM

Foxman,

I've tried to get your LineCursorGraph code to compile without success.   I'm getting errors relating to missing objects (e.g. Workspace, horizGrid).  Presumably I'm not setting up the xaml/cs correctly.

Any chance to posting the sample that shows this working?

Thanks

Grant.

May 19, 2010 at 11:49 AM

Adding the namespace to the x:Class class name fixed most of the compile errors except the Workspace error.  I've just commented out the Workspace related code and it now can be compiled and used.

It would still be handy to have a working sample as I don't yet understand how to use the LineCursorGraph.

May 19, 2010 at 3:17 PM

Sorry, I left some stuff that was specific to my aplication in there. Also, it was more meant as a guide for you develop your own version more than anything else. However, I will see what I can do for a sample and post something up.

Jul 26, 2010 at 9:39 PM

In case anyone is interested, here's a slightly modified version of Foxman's code the implements a cursor that you can drag & drop and/or bind to.

XAML:

<d3:ContentGraph x:Class="ChartExtensions.LineCursorGraph"
    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"
    xmlns:local="clr-namespace:ChartExtensions"
    IsHitTestVisible="false" Panel.ZIndex="1">
    <d3:ContentGraph.Resources>
        <Style x:Key="outerBorderStyle" TargetType="{x:Type Rectangle}" >
            <Setter Property="RadiusX" Value="10"/>
            <Setter Property="RadiusY" Value="10"/>
            <Setter Property="Stroke" Value="LightGray"/>
            <Setter Property="StrokeThickness" Value="1"/>
            <Setter Property="Fill" Value="#88FFFFFF"/>
        </Style>

        <Style x:Key="innerBorderStyle" TargetType="{x:Type Border}">
            <Setter Property="CornerRadius" Value="4"/>
            <!--<Setter Property="Background" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>-->
            <Setter Property="Margin" Value="8,4,8,4"/>
        </Style>

        <Style x:Key="textStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="2,1,2,1"/>
            <Setter Property="Foreground" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>
        </Style>

        <Style x:Key="lineStyle" TargetType="{x:Type Line}">
            <Setter Property="Stroke" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStroke}"/>
            <Setter Property="StrokeThickness" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStrokeThickness}"/>
            <Setter Property="StrokeDashArray" Value="{Binding 
				RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:LineCursorGraph}},
				Path=LineStrokeDashArray}"/>
            <Setter Property="IsHitTestVisible" Value="true"/>
        </Style>
    </d3:ContentGraph.Resources>
    <Canvas Name="content" Cursor="None" Background="Transparent" IsHitTestVisible="true">
        <Line Name="horizLine" Style="{StaticResource lineStyle}"/>
        <Line Name="vertLine" Style="{StaticResource lineStyle}"/>

        <Grid Name="horizGrid" Canvas.Top="10">
            <!--<Rectangle Style="{StaticResource outerBorderStyle}"/>-->
            <Border Style="{StaticResource innerBorderStyle}">
                <TextBlock Name="horizTextBlock" Style="{StaticResource textStyle}"/>
            </Border>
        </Grid>

        <Grid Name="vertGrid" Canvas.Left="5" Visibility="Collapsed">
            <Rectangle Style="{StaticResource outerBorderStyle}"/>
            <Border Style="{StaticResource innerBorderStyle}">
                <TextBlock Name="vertTextBlock" Style="{StaticResource textStyle}"/>
            </Border>
        </Grid>
    
    </Canvas>
</d3:ContentGraph>
    
    

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Microsoft.Research.DynamicDataDisplay.Charts;
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Research.DynamicDataDisplay;
using System.Diagnostics;

namespace ChartExtensions
{
    /// <summary>
    /// Interaction logic for LineCursorGraph.xaml
    /// </summary>
   
        public partial class LineCursorGraph : ContentGraph
        {
            Cursor DefaultParentCursor
            {
                get
                {
                    return Cursors.Arrow;
                }
            }

            #region Enums
            public enum CursorType
            {
                CursorA,
                CursorB,
                Waveform,
                Slicer
            }

            public enum LineCursorOrientation
            {
                Vertical,
                Horizontal
            }
            #endregion

            #region Dependency Properties
            #region CursorType
            public static readonly DependencyProperty CursorTypeProperty = DependencyProperty.Register("CursorType",
                typeof(CursorType), typeof(LineCursorGraph),
                new PropertyMetadata(CursorType.CursorA, OnCursorTypeChanged));

            public CursorType Type
            {
                get { return (CursorType)GetValue(CursorTypeProperty); }
                set { SetValue(CursorTypeProperty, value); }
            }

            private static void OnCursorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                LineCursorGraph line = (LineCursorGraph)d;
                line.OnCursorTypeChanged();
            }

            protected virtual void OnCursorTypeChanged()
            {
                switch (Type)
                {
                    case CursorType.CursorA:
                        //_parentCursor = Cursors.SizeNS;
                        LineStroke = Brushes.White;
                        Orientation = LineCursorOrientation.Vertical;
                        Canvas.SetTop(horizGrid, 10);
                        break;
                    default:
                    case CursorType.CursorB:
                        //_parentCursor = Cursors.SizeWE;
                        LineStroke = Brushes.Yellow;
                        Orientation = LineCursorOrientation.Vertical;
                        Canvas.SetTop(horizGrid, 50);
                        break;
                    case CursorType.Waveform:
                        LineStroke = Brushes.LimeGreen;
                        Orientation = LineCursorOrientation.Horizontal;
                        break;
                    case CursorType.Slicer:
                        LineStroke = Brushes.Red;
                        Orientation = LineCursorOrientation.Vertical;
                        break;
                }

                UpdateUIRepresentation();
            }
            #endregion

            #region Orientation
            public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation",
                typeof(LineCursorOrientation), typeof(LineCursorGraph),
                new PropertyMetadata(LineCursorOrientation.Vertical, OnOrientationChanged));

            public LineCursorOrientation Orientation
            {
                get { return (LineCursorOrientation)GetValue(OrientationProperty); }
                set { SetValue(OrientationProperty, value); }
            }

            private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                LineCursorGraph line = (LineCursorGraph)d;
                line.OnOrientationChanged();
            }

            protected virtual void OnOrientationChanged()
            {
                switch (Orientation)
                {
                    case LineCursorOrientation.Horizontal:
                        _parentCursor = Cursors.SizeNS;
                        break;
                    default:
                    case LineCursorOrientation.Vertical:
                        _parentCursor = Cursors.SizeWE;
                        break;
                }

                UpdateUIRepresentation();
            }
            #endregion

            #region XValue
            /// <summary>
            /// Identifies Value dependency property.
            /// </summary>
            public static readonly DependencyProperty XValueProperty =
                DependencyProperty.Register(
                  "XValue",
                  typeof(double),
                  typeof(LineCursorGraph),
                  new PropertyMetadata(
                      0.0, OnXValueChanged));


            /// <summary>
            /// Gets or sets the value in data coordinates
            /// </summary>
            public double XValue
            {
                get { return (double)GetValue(XValueProperty); }
                set { SetValue(XValueProperty, value); }
            }

            private static void OnXValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                LineCursorGraph line = (LineCursorGraph)d;
                line.OnXValueChanged();
            }

            protected virtual void OnXValueChanged()
            {
                UpdateUIRepresentation();

                if (SyncValue) // if this is an initial value set, then the other cursors should be synchronized
                {
                    RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
                    SyncValue = false;
                }
            }
            #endregion

            #region YValue
            /// <summary>
            /// Identifies Value dependency property.
            /// </summary>
            public static readonly DependencyProperty YValueProperty =
                DependencyProperty.Register(
                  "YValue",
                  typeof(double),
                  typeof(LineCursorGraph),
                  new PropertyMetadata(
                      0.0, OnYValueChanged));


            /// <summary>
            /// Gets or sets the value in data coordinates
            /// </summary>
            public double YValue
            {
                get { return (double)GetValue(YValueProperty); }
                set { SetValue(YValueProperty, value); }
            }

            private static void OnYValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                LineCursorGraph line = (LineCursorGraph)d;
                line.OnYValueChanged();
            }

            protected virtual void OnYValueChanged()
            {
                UpdateUIRepresentation();

                //if (SyncValue) // if this is an initial value set, then the other cursors should be synchronized
                //{
                //    RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
                //    SyncValue = false;
                //}
            }
            #endregion
            #endregion

            #region Public Properties
            private string customXFormat = null;
            /// <summary>
            /// Gets or sets the custom format string of x label.
            /// </summary>
            /// <value>The custom X format.</value>
            public string CustomXFormat
            {
                get { return customXFormat; }
                set
                {
                    if (customXFormat != value)
                    {
                        customXFormat = value;
                        UpdateUIRepresentation();
                    }
                }
            }

            private string customYFormat = null;
            /// <summary>
            /// Gets or sets the custom format string of y label.
            /// </summary>
            /// <value>The custom Y format.</value>
            public string CustomYFormat
            {
                get { return customYFormat; }
                set
                {
                    if (customYFormat != value)
                    {
                        customYFormat = value;
                        UpdateUIRepresentation();
                    }
                }
            }

            private bool showHorizontalLine = true;
            /// <summary>
            /// Gets or sets a value indicating whether to show horizontal line.
            /// </summary>
            /// <value><c>true</c> if horizontal line is shown; otherwise, <c>false</c>.</value>
            public bool ShowHorizontalLine
            {
                get { return showHorizontalLine; }
                set
                {
                    if (showHorizontalLine != value)
                    {
                        showHorizontalLine = value;
                        UpdateVisibility();
                    }
                }
            }

            private bool showVerticalLine = true;
            /// <summary>
            /// Gets or sets a value indicating whether to show vertical line.
            /// </summary>
            /// <value><c>true</c> if vertical line is shown; otherwise, <c>false</c>.</value>
            public bool ShowVerticalLine
            {
                get { return showVerticalLine; }
                set
                {
                    if (showVerticalLine != value)
                    {
                        showVerticalLine = value;
                        UpdateVisibility();
                    }
                }
            }

            private bool showCursorText = true;
            /// <summary>
            /// Gets or sets a value indicating whether to display the cursor text.
            /// </summary>
            public bool ShowCursorText
            {
                get { return showCursorText; }
                set
                {
                    showCursorText = value;
                    if (showCursorText)
                        horizGrid.Visibility = Visibility.Visible;
                    else
                        horizGrid.Visibility = Visibility.Collapsed;
                }

            }

            public bool SyncValue { get; set; }

            #endregion

            #region Private Fields
            FrameworkElement _parent;

            Vector blockShift = new Vector(3, 3);

            bool _drag = false;

            Cursor _parentCursor = Cursors.SizeWE;
            #endregion

            public static RoutedEvent CursorMovedEvent = EventManager.RegisterRoutedEvent("CursorMoved",
                RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(LineCursorGraph));


            public event RoutedEventHandler CursorMoved
            {
                add { AddHandler(CursorMovedEvent, value); }
                remove { RemoveHandler(CursorMovedEvent, value); }
            }

            public LineCursorGraph()
            {
                InitializeComponent();

                CursorMoved += new RoutedEventHandler(CursorMovedEventHandler);

                // default settings
                Orientation = LineCursorOrientation.Vertical;
                //LineStroke = Brushes.Black;
            }

            protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
            {
                base.OnMouseLeftButtonDown(e);
            }

            protected override void OnPlotterAttached()
            {
                base.OnPlotterAttached();

                _parent = (FrameworkElement)Parent;

                _parent.MouseMove += new MouseEventHandler(parent_MouseMove);
                _parent.MouseEnter += new MouseEventHandler(parent_MouseEnter);
                _parent.MouseLeave += new MouseEventHandler(parent_MouseLeave);
                _parent.MouseLeftButtonDown += new MouseButtonEventHandler(parent_MouseLeftButtonDown);
                _parent.MouseLeftButtonUp += new MouseButtonEventHandler(parent_MouseLeftButtonUp);

                UpdateUIRepresentation();
            }

            void parent_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
            {
                _drag = false;
                _parent.Cursor = DefaultParentCursor;
                _parent.ReleaseMouseCapture();
            }

            void parent_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
            {
                if (IsMouseNearLine(e.GetPosition(this)))
                {
                    _drag = true;
                    _parent.CaptureMouse();
                }
            }

            void parent_MouseLeave(object sender, MouseEventArgs e)
            {
                _parent.ReleaseMouseCapture();
            }

            void parent_MouseEnter(object sender, MouseEventArgs e)
            {
            }

            void parent_MouseMove(object sender, MouseEventArgs e)
            {
                if (!_drag)
                {
                    if (IsMouseNearLine(e.GetPosition(this)))
                    {
                        _parent.Cursor = _parentCursor;
                        e.Handled = true;
                    }
                    else
                    {
                        _parent.Cursor = DefaultParentCursor;
                    }
                }

                if (_drag)
                {
                    double value = 0;
                    if (Orientation == LineCursorOrientation.Vertical)
                    {
                        value = e.GetPosition(Plotter2D.CentralGrid).ScreenToData(Plotter2D.Viewport.Transform).X;
                        if ( Plotter2D.Viewport.Visible.XMin <= value &&
                             Plotter2D.Viewport.Visible.XMax >= value )
                            XValue = value;
                    }
                    else
                        XValue = e.GetPosition(Plotter2D.CentralGrid).ScreenToData(Plotter2D.Viewport.Transform).Y;

                    RaiseEvent(new RoutedEventArgs(CursorMovedEvent));
                }
            }

            bool IsMouseNearLine(Point mousePos)
            {
                Point dataPoint = mousePos.ScreenToData(Plotter2D.Viewport.Transform);
                if (Orientation == LineCursorOrientation.Vertical)
                {
                    Point cursorPoint = new Point(XValue, 0);
                    Point screenPoint = cursorPoint.DataToScreen(Plotter2D.Viewport.Transform);
                    return Math.Abs(screenPoint.X - mousePos.X) < 5;
                }
                else
                {
                    Point cursorPoint = new Point(0, YValue);
                    Point screenPoint = cursorPoint.DataToScreen(Plotter2D.Viewport.Transform);
                    return Math.Abs(screenPoint.Y - mousePos.Y) < 5;
                }
            }

            void CursorMovedEventHandler(object sender, RoutedEventArgs e)
            {
                LineCursorGraph cursor = (LineCursorGraph)sender;
                if (this != cursor && this.Type == cursor.Type ) 
                {
                    XValue = cursor.XValue;
                    //e.Handled = true;
                }
            }

            protected override void OnViewportPropertyChanged(ExtendedPropertyChangedEventArgs e)
            {
                UpdateUIRepresentation();
            }

            protected override void OnPlotterDetaching()
            {
                base.OnPlotterDetaching();
            }

            protected void UpdateUIRepresentation()
            {
                if (Plotter2D == null) return;

                var transform = Plotter2D.Viewport.Transform;
                DataRect visible = Plotter2D.Viewport.Visible;
                Rect output = Plotter2D.Viewport.Output;

                Point dataPosition = new Point(XValue, 0);
                Point actualPosition = dataPosition.DataToScreen(transform);

                vertLine.X1 = actualPosition.X;
                vertLine.X2 = actualPosition.X;
                vertLine.Y1 = output.Top;
                vertLine.Y2 = output.Bottom;
            }

            private string GetRoundedValue(double min, double max, double value)
            {
                double roundedValue = value;
                //var log = RoundingHelper.GetDifferenceLog(min, max);
                var log = (int)Math.Round(Math.Log10(Math.Abs(max - min)));
                string format = "G3";
                double diff = Math.Abs(max - min);
                if (1E3 < diff && diff < 1E6)
                {
                    format = "F0";
                }
                if (log < 0)
                    format = "G" + (-log + 2).ToString();

                return roundedValue.ToString(format);
            }

            private void UpdateVisibility()
            {
                horizLine.Visibility = vertGrid.Visibility = GetHorizontalVisibility();
                vertLine.Visibility = horizGrid.Visibility = GetVerticalVisibility();
            }

            private Visibility GetHorizontalVisibility()
            {
                return showHorizontalLine ? Visibility.Visible : Visibility.Hidden;
            }

            private Visibility GetVerticalVisibility()
            {
                return showVerticalLine ? Visibility.Visible : Visibility.Hidden;
            }

            #region LineStroke property

            public Brush LineStroke
            {
                get { return (Brush)GetValue(LineStrokeProperty); }
                set { SetValue(LineStrokeProperty, value); }
            }

            public static readonly DependencyProperty LineStrokeProperty = DependencyProperty.Register(
              "LineStroke",
              typeof(Brush),
              typeof(LineCursorGraph),
              new PropertyMetadata( Brushes.Black));

            #endregion

            #region LineStrokeThickness property

            public double LineStrokeThickness
            {
                get { return (double)GetValue(LineStrokeThicknessProperty); }
                set { SetValue(LineStrokeThicknessProperty, value); }
            }

            public static readonly DependencyProperty LineStrokeThicknessProperty = DependencyProperty.Register(
              "LineStrokeThickness",
              typeof(double),
              typeof(LineCursorGraph),
              new PropertyMetadata(2.0));

            #endregion

            #region LineStrokeDashArray property

            [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
            public DoubleCollection LineStrokeDashArray
            {
                get { return (DoubleCollection)GetValue(LineStrokeDashArrayProperty); }
                set { SetValue(LineStrokeDashArrayProperty, value); }
            }

            public static readonly DependencyProperty LineStrokeDashArrayProperty = DependencyProperty.Register(
              "LineStrokeDashArray",
              typeof(DoubleCollection),
              typeof(LineCursorGraph),
              new FrameworkPropertyMetadata(DoubleCollectionHelper.Create(3, 3)));

            #endregion
        }
}

 

 

Sep 10, 2010 at 9:39 AM
Edited Sep 10, 2010 at 2:20 PM

I made two new Wpf user controls in my program called CursorH and CursorV to have some horizontal and vertical cursors and it worked. This is my code for horizontal cursors.

Regards

Leandro

CursorH.xaml

    <d3:PositionalViewportUIContainer x:Class="Microsoft.Research.DynamicDataDisplay.Charts.Shapes.CursorH"
    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"
        xmlns:d3b="clr-namespace:Microsoft.Research.DynamicDataDisplay.Charts;assembly=DynamicDataDisplay"
        ToolTip="{Binding Position, RelativeSource={RelativeSource Self}}" Height="80" Width="897">
            <d3:PositionalViewportUIContainer.Style>
                <Style TargetType="{x:Type d3:PositionalViewportUIContainer}">
                    <Style.Resources>
                        <Storyboard x:Key="story">
                        </Storyboard>
                    </Style.Resources>

                    <Setter Property="Focusable" Value="False"/>
                    <Setter Property="Opacity" Value="10"/>
                    <Setter Property="Cursor" Value="ScrollAll"/>
                    <Setter Property="HorizontalContentAlignment" Value="Center"/>
                    <Setter Property="VerticalContentAlignment" Value="Center"/>

                    <Style.Triggers>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsMouseOver" Value="True"/>
                                <Condition Property="IsMouseCaptured" Value="False"/>
                            </MultiTrigger.Conditions>
                            <MultiTrigger.EnterActions>
                                <BeginStoryboard Name="storyboard" Storyboard="{StaticResource story}"/>
                            </MultiTrigger.EnterActions>
                            <MultiTrigger.ExitActions>
                                <RemoveStoryboard BeginStoryboardName="storyboard"/>
                            </MultiTrigger.ExitActions>
                        </MultiTrigger>
                    </Style.Triggers>
                </Style>
            </d3:PositionalViewportUIContainer.Style>
            <Rectangle Name = "cursorgraph"  Fill="Gray" Stroke="Transparent" Margin="0,0,0,0" Grid.ColumnSpan="5" Height="4" VerticalAlignment="Center"/>
        </d3:PositionalViewportUIContainer>


// CursorH.xaml.cs
       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;
using Microsoft.Research.DynamicDataDisplay;

namespace Microsoft.Research.DynamicDataDisplay.Charts.Shapes
{
    /// <summary>
    /// Interaction logic for CursorH.xaml
    /// </summary>
    public partial class CursorH : Microsoft.Research.DynamicDataDisplay.Charts.PositionalViewportUIContainer
    {
        HorizontalLine hLine = new HorizontalLine();
        ChartPlotter cPlotter = null;

        public CursorH()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CursorH"/> class.
        /// </summary>
        /// <param name="position">The position of CursorH.</param>
        public CursorH(Point position) : this()
        {
            Position = position;
        }

        bool dragging = false;
        Point dragStart;
        Vector shift;

        private void setCursor()
        {
            if (cPlotter != null)
            {
                Width = cPlotter.Viewport.Visible.XMax - cPlotter.Viewport.Visible.XMin;
                cursorgraph.Width = hLine.Width;
            }
        }
       
        public void SetCursor(ChartPlotter plotter)
        {
            Visibility = Visibility.Visible;
            cPlotter = plotter;
            setCursor();
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            if (Plotter == null)
                return;

            dragStart = e.GetPosition(Plotter.ViewportPanel).ScreenToData(Plotter.Viewport.Transform);
            setCursor();
            shift = Position - dragStart;
            dragging = true;
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            ReleaseMouseCapture();
            dragging = false;
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (!dragging)
            {
                if (IsMouseCaptured)
                    ReleaseMouseCapture();

                return;
            }

            if (!IsMouseCaptured)
                CaptureMouse();

            Point mouseInData = e.GetPosition(Plotter.ViewportPanel).ScreenToData(Plotter.Viewport.Transform);

            if (mouseInData != dragStart)
            {
                Position = mouseInData + shift;
                setCursor();
                e.Handled = true;
            }
        }

        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            if (dragging)
            {
                dragging = false;
                if (IsMouseCaptured)
                {
                    ReleaseMouseCapture();
                    hLine.Value = Position.Y;
                    e.Handled = true;
                }
            }
        }

        protected override Size ArrangeOverride(Size arrangeBounds)
        {
            return base.ArrangeOverride(arrangeBounds);
        }
   
    }
}


// implementation in a Window.xaml.cs file

        CursorH hCursor1 = new CursorH(new Point(0,0));

        // after data loaded
       
            Point pCursorH = new Point(0, 1).DataToScreen(Plotter.Viewport.Transform);
            hCursor1.Position = pCursorH;
            hCursor1.ToolTip = "H. cursor 1";
            Plotter.Children.Add(hCursor1);
            hCursor1.SetCursor(Plotter);

            hCursor1.Visibility = Visibility.Collapsed;
       

            // to show/hide the cursor
            hCursor1.Visibility = Visibility.Visible;
            hCursor1.Visibility = Visibility.Hidden;
       
       

Sep 10, 2010 at 2:17 PM
McBainUK wrote:
Thecentury wrote:

I think the thing you want can be created from VerticalLine and DraggablePoint, with line's x coordinate bound to point's position.X. You really can make a try to create a custom template for draggable point so that there would be no circle with point in its center, but a line or smth like that.

I came to the same conclusion. Merging the line with a draggable point and using a custom template looks like it's working. Thanks Mikhail.