1

Currently my software uses the HitTest() method of a chart object in MSCharts but as I scale this up to more and more data points on my chart combined with other factors this can have a massive performance hit.

I was wondering if there any alternatives that you know of to provide the same functionality ( get the X Coordinate on the chart for the cursor position ) but without the performance hit as hit testing seems to be a very brute force way of obtaining my answer.

My chart is created from the class System.Windows.Forms.DataVisualization.Charting.Chart

Edit for clarity: I need to find the position of a line on my chart to use it for other calculations.

Chris Tompkinson
  • 588
  • 7
  • 18
  • A HitTest() function should only ever be used in mouse event handling code. That runs at human time, burning dozens of milliseconds is not a problem. If it takes longer than that then you are just stuffing way too much data in the chart, much more than it ever could display in detail. So filter your data better. – Hans Passant Jul 18 '13 at 12:57
  • The chart requires the amount of data, the HitTest is used to determine chart information displayed next to the cursor so that the user can run there mouse along the chart and effectively see accurate data at their mouse location. Although maybe a solution would be to take an average set of datapoints instead of using so many, but that loss of precision is not ideal. – Chris Tompkinson Jul 18 '13 at 17:44
  • A mouse only has pixel accuracy. So you never require more data points than there are pixels across the screen. Also do consider what that means for your intended feature, the user can never see the data for two points that overlap in the graph. Last but not least, you can easily ensure the data points are sorted, allowing a binary search to locate the data point. An O(log n) algorithm, very fast. – Hans Passant Jul 18 '13 at 17:58

1 Answers1

3

Had the same performance issue with a mousewheel event.

Here is my solution:

  1. To get the axes values of the current mouse position:

    double posX = Math.Round(currentArea.AxisX.PixelPositionToValue(e.X));
    double posY = Math.Round(currentArea.AxisY.PixelPositionToValue(e.Y));
    

    Taken from Showing Mouse Axis Coordinates on Chart Control with a little change to get it more accurate.

    But you should check before, that the mouse is in a ChartArea, else it will throw you an Exception.

  2. To get the ChatElement on which the mouse points:

    // Gets the ChartArea that the mouse points
    private ChartArea mouseinChartArea(Chart source, Point e)
    {
        double relativeX = (double)e.X * 100 / source.Width;
        double relativeY = (double)e.Y * 100 / source.Height;
    
        foreach (ChartArea ca in source.ChartAreas)
        {
            if (relativeX > ca.Position.X && relativeX < ca.Position.Right &&
                relativeY > ca.Position.Y && relativeY < ca.Position.Bottom)
                return ca;
        }
        return null;
    }
    
    // for my purpose, returns an axis. But you can return anything
    private Axis findAxisforZooming(Chart source, Point e)
    {
        ChartArea currentArea = mouseinChartArea(source, new Point(e.X, e.Y)); // Check if inside 
        if (currentArea == null)
            return null;
    
        double axisXfontSize = currentArea.AxisX.LabelAutoFitMinFontSize + ((double)source.Width / SystemInformation.PrimaryMonitorSize.Width)
            * (currentArea.AxisX.LabelAutoFitMaxFontSize - currentArea.AxisX.LabelAutoFitMinFontSize);
        double axisYfontSize = currentArea.AxisY.LabelAutoFitMinFontSize + ((double)source.Height / SystemInformation.PrimaryMonitorSize.Height)
            * (currentArea.AxisY.LabelAutoFitMaxFontSize - currentArea.AxisY.LabelAutoFitMinFontSize);
        double axisYfontHeightSize = (axisYfontSize - currentArea.AxisY.LabelStyle.Font.Size) + currentArea.AxisY.LabelStyle.Font.Height;
    
        Graphics g = this.CreateGraphics();
        if (currentArea.AxisX.LabelStyle.Font.Unit == GraphicsUnit.Point)
            axisXfontSize = axisXfontSize * g.DpiX / 72;
        if (currentArea.AxisY.LabelStyle.Font.Unit == GraphicsUnit.Point)
            axisYfontHeightSize = axisYfontHeightSize * g.DpiX / 72;
        g.Dispose();
    
        // Replacing the SystemInformation.PrimaryMonitorSize with the source.Width / Height will give the accurate TickMarks size.
        // But it doens't count for the gab between the tickMarks and the axis lables (so by replacing, it give a good proximity with the gab)
        int axisYTickMarks = (int)Math.Round(currentArea.AxisY.MajorTickMark.Size / 100 * SystemInformation.PrimaryMonitorSize.Width); // source.Width;
        int axisXTickMarks = (int)Math.Round(currentArea.AxisX.MajorTickMark.Size / 100 * SystemInformation.PrimaryMonitorSize.Height); // source.Height;
    
        int leftInnerPlot = (int)Math.Round(currentArea.Position.X / 100 * source.Width +
            currentArea.InnerPlotPosition.X / 100 * currentArea.Position.Width / 100 * source.Width);
        int rightInnerPlot = (int)Math.Round(currentArea.Position.X / 100 * this.chart1.Width +
            currentArea.InnerPlotPosition.Right / 100 * currentArea.Position.Width / 100 * source.Width);
        int topInnerPlot = (int)Math.Round(currentArea.Position.Y / 100 * this.chart1.Height +
            currentArea.InnerPlotPosition.Y / 100 * currentArea.Position.Height / 100 * source.Height);
        int bottomInnerPlot = (int)Math.Round(currentArea.Position.Y / 100 * source.Height +
            currentArea.InnerPlotPosition.Bottom / 100 * currentArea.Position.Height / 100 * source.Height);
    
        // Now you got the boundaries of every important ChartElement.
        // Only left to check if the mouse is within your desire ChartElement,
        // like the following:    
    
        bottomInnerPlot += axisXTickMarks + (int)Math.Round(axisXfontSize); // Include AxisX
    
        if (e.X > leftInnerPlot && e.X < rightInnerPlot &&
            e.Y > topInnerPlot && e.Y < bottomInnerPlot) // return AxisX if inside the InnerPlot area or on AxisX
            return currentArea.AxisX;
        else if (e.X > (leftInnerPlot - axisYTickMarks - (int)Math.Round(axisYfontHeightSize)) && e.X < rightInnerPlot &&
                 e.Y > topInnerPlot && e.Y < bottomInnerPlot) // return AxisY if on AxisY only
            return currentArea.AxisY;
    
        return null;
    }
    

As it can be seen, the code is longer than HitTest(). But the run time is shorter.

Community
  • 1
  • 1
Anidor
  • 46
  • 5