Search code examples
wpflivecharts

Wpf Live-Charts display tooltip based on mouse cursor move without mouse hover over Line chart point


I am using WPF Live-Charts (https://lvcharts.net)
I want the tooltip to display the point value according to the mouse cursor movement, as in the image link below.
I tried, but I haven't found a way to display the tooltip without hovering the mouse cursor over the point in Live-Charts.

Examples:

enter image description here

enter image description here

If anyone has done this, can you give some advice?


Solution

  • The solution is relatively simple. The problem with LiveCharts is, that it not well documented. It gets you easily started by providing some examples that target general requirements. But for advanced scenarios, the default controls doesn't offer enough flexibility to customize the behavior or layout. There is no documentation about the details on how things work or what the classes of the library are intended for.

    Once I checked the implementation details, I found the controls to be really horrible authored or designed.

    Anyway, this simple feature you are requesting is a good example for the shortcomings of the library - extensibility is really bad. Even customization is bad. I wish the authors would have allowed templates, as this would make customization a lot easier. It should be simple to to extend the existing behavior, but apparently its not, unless you know about undocumented implementation details.
    The library doesn't come in like a true WPF library. I don't know the history, maybe it's a WinForms port by WinForms devs. But it's free and open source. And that's a big plus.


    The following example draws a cursor on the plotting area which snaps to the nearest chart point and higlights it, while the mouse is moving. A custom ToolTip follows the mouse pointer to show info about the currently selected chart point:

    ViewModel.cs

    public class ViewModel : INotifyPropertyChanged
    {
      public ViewModel()
      {
        var chartValues = new ChartValues<Point>();
        
        // Create a sine
        for (double x = 0; x < 361; x++)
        {          
          var point = new Point() {X = x, Y = Math.Sin(x * Math.PI / 180)}; 
          chartValues.Add(point);
        }
    
        SeriesCollection = new SeriesCollection
        {          
          new LineSeries
          {
            Configuration = new CartesianMapper<Point>()
              .X(point => point.X)
              .Y(point => point.Y),
            Title = "Series X",
            Values = chartValues,
            Fill = Brushes.DarkRed
          }
        };
      }
    
      private ChartPoint selectedChartPoint;
      public ChartPoint SelectedChartPoint
      {
        get => this.selectedChartPoint;
        set
        {
          this.selectedChartPoint = value;
          OnPropertyChanged();
        }
      }
    
      private double cursorScreenPosition;
      public double CursorScreenPosition
      {
        get => this.cursorScreenPosition;
        set
        {
          this.cursorScreenPosition = value;
          OnPropertyChanged();
        }
      }
    
      public SeriesCollection SeriesCollection { get; set; }
    
      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
    }
    

    MainWindow.xaml.cs

    partial class MainWindow : Window
    { 
      private void MoveChartCursorAndToolTip_OnMouseMove(object sender, MouseEventArgs e)
      {
        var chart = sender as CartesianChart;
    
        if (!TryFindVisualChildElement(chart, out Canvas outerCanvas) ||
            !TryFindVisualChildElement(outerCanvas, out Canvas graphPlottingArea))
        {
          return;
        }
        
        var viewModel = this.DataContext as ViewModel;
        Point chartMousePosition = e.GetPosition(chart);
    
        // Remove visual hover feedback for previous point
        viewModel.SelectedChartPoint?.View.OnHoverLeave(viewModel.SelectedChartPoint);
    
        // Find current selected chart point for the first x-axis
        Point chartPoint = chart.ConvertToChartValues(chartMousePosition);
        viewModel.SelectedChartPoint = chart.Series[0].ClosestPointTo(chartPoint.X, AxisOrientation.X);
    
        // Show visual hover feedback for previous point
        viewModel.SelectedChartPoint.View.OnHover(viewModel.SelectedChartPoint);
    
        // Add the cursor for the x-axis.
        // Since Chart internally reverses the screen coordinates
        // to match chart's coordinate system
        // and this coordinate system orientation applies also to Chart.VisualElements,
        // the UIElements like Popup and Line are added directly to the plotting canvas.
        if (chart.TryFindResource("CursorX") is Line cursorX 
          && !graphPlottingArea.Children.Contains(cursorX))
        {
          graphPlottingArea.Children.Add(cursorX);
        }
    
        if (!(chart.TryFindResource("CursorXToolTip") is FrameworkElement cursorXToolTip))
        {
          return;
        }
    
        // Add the cursor for the x-axis.
        // Since Chart internally reverses the screen coordinates
        // to match chart's coordinate system
        // and this coordinate system orientation applies also to Chart.VisualElements,
        // the UIElements like Popup and Line are added directly to the plotting canvas.
        if (!graphPlottingArea.Children.Contains(cursorXToolTip))
        {
          graphPlottingArea.Children.Add(cursorXToolTip);
        }
    
        // Position the ToolTip
        Point canvasMousePosition = e.GetPosition(graphPlottingArea);
        Canvas.SetLeft(cursorXToolTip, canvasMousePosition.X - cursorXToolTip.ActualWidth);
        Canvas.SetTop(cursorXToolTip, canvasMousePosition.Y);      
      }
    
      // Helper method to traverse the visual tree of an element
      private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
        where TChild : DependencyObject
      {
        resultElement = null;
        for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
        {
          DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
    
          if (childElement is Popup popup)
          {
            childElement = popup.Child;
          }
    
          if (childElement is TChild)
          {
            resultElement = childElement as TChild;
            return true;
          }
    
          if (TryFindVisualChildElement(childElement, out resultElement))
          {
            return true;
          }
        }
    
        return false;
      }
    }
    

    MainWindow.xaml

    <Window>
      <Window.DataComtext>
        <ViewModel />
      </Window.DataContext>
    
      <CartesianChart MouseMove="MoveChartCursorAndToolTip_OnMouseMove"
                      Series="{Binding SeriesCollection}"
                      Zoom="X"
                      Height="600">
        <CartesianChart.Resources>
    
          <!-- The cursor for the x-axis that snaps to the nearest chart point -->
          <Line x:Key="CursorX"
                Canvas.ZIndex="2"
                Canvas.Left="{Binding SelectedChartPoint.ChartLocation.X}"
                Y1="0"
                Y2="{Binding ElementName=CartesianChart, Path=ActualHeight}"
                Stroke="Gray"
                StrokeThickness="1" />
    
          <!-- The ToolTip that follows the mouse pointer-->
          <Border x:Key="CursorXToolTip"
                  Canvas.ZIndex="3"
                  Background="LightGray"
                  Padding="8"
                  CornerRadius="8">
            <StackPanel Background="LightGray">
              <StackPanel Orientation="Horizontal">
                <Path Height="20" Width="20"
                      Stretch="UniformToFill"
                      Data="{Binding SelectedChartPoint.SeriesView.(Series.PointGeometry)}"
                      Fill="{Binding SelectedChartPoint.SeriesView.(Series.Fill)}"
                      Stroke="{Binding SelectedChartPoint.SeriesView.(Series.Stroke)}"
                      StrokeThickness="{Binding SelectedChartPoint.SeriesView.(Series.StrokeThickness)}" />
    
                <TextBlock Text="{Binding SelectedChartPoint.SeriesView.(Series.Title)}"
                           VerticalAlignment="Center" />
              </StackPanel>
              <TextBlock Text="{Binding SelectedChartPoint.X, StringFormat=X:{0}}" />
              <TextBlock Text="{Binding SelectedChartPoint.Y, StringFormat=Y:{0}}" />
            </StackPanel>
          </Border>
        </CartesianChart.Resources>
        <CartesianChart.AxisY>
          <Axis Title="Y" />
        </CartesianChart.AxisY>
        <CartesianChart.AxisX>
          <Axis Title="X" />
        </CartesianChart.AxisX>
      </CartesianChart>
    <Window>