0

I want to create a zoom picturebox like in PictureBox Zoom for mschart. When I hover the mouse on the chart, the picturebox should show zoom view, so that I can select an appropriate point. Can this happen in winforms C#.net?

user2587
  • 149
  • 10
  • 2
    Yes the mschart supports zooming and mouse events well. Show what you tried! as it stands the question is way too broad and shows no research effort.. Note that I would use two chart controls, no pbox. – TaW Nov 26 '18 at 08:28
  • @TaW This means I have to use another chart control instead of picturebox. If that's so I will start working on it. – user2587 Nov 26 '18 at 08:38
  • 1
    Yes, that seems the most direct approach. Otherwise you would have to convert the mouse positions in the pbox to chart pixel and these to points. And then create an enlarged image to display. Or start with a large image and change the zoom. Also doable but more involved.. – TaW Nov 26 '18 at 09:15
  • Upon rethinking the issue I wonder: Do you mean you want to mouve the mouse over the chart or the zoomed area? For the former maybe a pbox may indeed be ok.. – TaW Nov 26 '18 at 12:30
  • @TaW As I want to move the mouse over the chart, the picturebox should show the zoomed area where mouse is moving. I did it with chart control. Its working ! – user2587 Nov 26 '18 at 14:28
  • Great! I'm trying to get the original idea to work correctly but it isn't so simple to actually get the position centerted exactly.. – TaW Nov 26 '18 at 15:00
  • @TaW True, it was easier than I thought :) – user2587 Nov 27 '18 at 03:45
  • 1
    If you are happy with it you may want to consider posting your solution; I'm sure to upvote it. I have posted my solution for the original idea. Note that imo the best help for selecting DataPoints is indicating the points with hittests and enlarging markers.. Bit that can probably be combined with either solution.. – TaW Nov 27 '18 at 21:03

2 Answers2

1

Here is a solution that does what op asked for: Create a PictureBox that shows a zoomed portion of a Chart which will move as one moves across the chart.

It looks fine but one will still have to move across those tiny unzoomed pixels..

Here is how is done and set up:

Whenever necessary the PictureBox zoomPBox must be set up again; setting it up involves taking a few measurements and creating a screenshot of the Chart. For this the chart is enlarged temporarily and then reset to the original size.

Note: Whenever the chart get resized or changed in any other way the setup routine must be called again.

The PictureBox zoomPBox is set to SizeMode Normal and is nested in a Panel. In the setup we enlarge the zoomPBox to hold the whole Bitmap. The Panel zoomPanel has AutoScroll = false to avoid scrollbars.

One complication is the automatic sizing the Chart controls does. When enlarging it the content is enlarged but e.g. none of the fonts are. This leads to different aspect ratios between the normal and the zoomed plot area. To keep movement in synch we can't have this. Therefore we not only want to cut out the actual inner plot area without Legend, Title or Axes from the zoomed screenshot but also stretch it to the same aspect ratio as the unzoomed plot area..

Here is the result:

enter image description here

The code for the MouseMove is not so involved..:

private void chart_MouseMove(object sender, MouseEventArgs e)
{
    if (zoomPBox.Image == null) return;

    Rectangle ri = Rectangle.Round(
                    InnerPlotPositionClientRectangle(chart, chart.ChartAreas[0]));

    Size szi = zoomPBox.Image.Size;
    Size szp = zoomPanel.ClientSize;
    Point cp = new Point( e.X - ri.X ,  e.Y - ri.Y );
    float zx = 1f * szi.Width / ri.Width;
    float zy = 1f * szi.Height / ri.Height;  // should be the same
    int x = round( szp.Width / 2 - cp.X * zx );
    int y = round( szp.Height / 2 - cp.Y * zy );
    zoomPBox.Location = new Point(x, y);     // now we move the pBox into position
    zoomPBox.Invalidate();
}

As you can see I Invalidate the PictureBox; that is to allow it to draw crosshair lines onto itself for better control; here is the Paint event:

private void zoomPBox_Paint(object sender, PaintEventArgs e)
{
    Size sz = zoomPanel.ClientSize;
    int x = sz.Width / 2 - zoomPBox.Left;
    int y = sz.Height / 2 - zoomPBox.Top;
    e.Graphics.DrawLine(Pens.LightGray, 0, y, zoomPBox.Width, y);
    e.Graphics.DrawLine(Pens.LightGray, x, 0, x, zoomPBox.Height);
}

Now for the setup routine:

    void setupZoomBox(Chart chart, PictureBox pbox, float zoom)
    {
        ChartArea ca = chart.ChartAreas[0];
        Size sz = chart.ClientSize;
        Size szi = new Size(round(sz.Width * zoom), round(sz.Height * zoom));
        Bitmap bmp2 = null;
        chart.Refresh();

        // original plot area
        Rectangle pao = Rectangle.Round(InnerPlotPositionClientRectangle(chart, ca));
        float ro = 1f * (pao.Width+2) / (pao.Height+2);  // original aspect ratio

        chart.ClientSize = szi;
        chart.Refresh();  // enforce immediate layout
        // zoomed plot area
        Rectangle paz = Rectangle.Round(InnerPlotPositionClientRectangle(chart, ca));
        float rz = 1f * paz.Width / paz.Height;   // zoomed aspect ratio

        // target rectangle, same aspect ratio as unzoomed  area
        int th = paz.Height;
        int tw = round(paz.Height * ro );
        // if (ro > rz)
            //tw = round(th * ro); //else th = round(tw / ro);
        Rectangle tgtR = new Rectangle(0, 0, tw, th);

        // bitmap to hold only the zoomed inner plot area
        bmp2 = new Bitmap(tgtR.Width, tgtR.Height);

        // source area: Only the inner plot area plus 1 line of axis pixels:
        Rectangle srcR = Rectangle.Round(
                           new RectangleF(paz.X - 1, paz.Y - 1, paz.Width + 2, paz.Height + 2));

        // bitmap to hold the whole zoomed chart:
        using (Bitmap bmp = new Bitmap(szi.Width, szi.Height))
        {
            Rectangle drawR = new Rectangle(0, 0, szi.Width, szi.Height);
            chart.DrawToBitmap(bmp, drawR);  // screenshot
            using (Graphics g = Graphics.FromImage(bmp2))  // crop stretched
                 g.DrawImage(bmp, tgtR, srcR, GraphicsUnit.Pixel);
        }
        chart.ClientSize = sz;  // reset chart
        // you should dispose of the old Image if there is one before setting the new one!!
        pbox.Image = bmp2;    
        pbox.ClientSize = bmp2.Size;
    }

At a few spots I need to get the pixel size of the so called InnerPlotPosition; (ElementPosition in a MSChart includes Location and Size in percentages of the respective container area.) I use functions I have posted before, e.g. here.

TaW
  • 53,122
  • 8
  • 69
  • 111
1

Another solution is using chart control as a zoom view. You can view the data point marked in red when the mouse moves over the original chart. As shown below: Using chart control for zoom

Here is the code :

private void chart1_MouseMove(object sender, MouseEventArgs e)
    {
        Point mousePoint = new Point(e.X, e.Y);
        double mouse_Xvalue = chart1.ChartAreas[0].AxisX.PixelPositionToValue(e.X);
        double mouse_Yvalue = chart1.ChartAreas[0].AxisY.PixelPositionToValue(e.Y);

        DataPoint Prev_DataPoint = chart1.Series[0].Points.Select(x => x)
                                        .Where(x => x.XValue >= mouse_Xvalue)
                                        .DefaultIfEmpty(chart1.Series[0].Points.First()).First();

        DataPoint Next_DataPoint = chart1.Series[0].Points.Select(x => x)
                              .Where(x => x.XValue <= mouse_Xvalue)
                               .DefaultIfEmpty(chart1.Series[0].Points.Last()).Last();

        double diff_prev = Math.Abs(Prev_DataPoint.XValue - mouse_Xvalue);
        double diff_next = Math.Abs(Next_DataPoint.XValue - mouse_Xvalue);

        int zoffset = 15;
        int setindexX = diff_prev < diff_next ?
                          chart1.Series[0].Points.IndexOf(Prev_DataPoint)
                        : chart1.Series[0].Points.IndexOf(Next_DataPoint);

        int setXmin = (setindexX - zoffset) >= 0 ? (setindexX - zoffset)
                        : 0;
        int setXmax = (setindexX + zoffset) < chart1.Series[0].Points.Count
                      ? (setindexX + zoffset)
                        : chart1.Series[0].Points.Count - 1;

        if (zoomchart.Series.Count > 0)
            zoomchart.Series.Clear();

        Series series = new Series();
        Series series2 = new Series();
        series.Points.Clear();
        series2.Points.Clear();

        for (int i = setXmin; i <= setXmax; i++)
            series.Points.AddXY(chart1.Series[0].Points[i].XValue,
                                chart1.Series[0].Points[i].YValues[0]);
        series.Color = chart1.Series[0].Color;
        series.ChartType = SeriesChartType.Line;

        series2.Points.AddXY(chart1.Series[0].Points[setindexX].XValue,
                             chart1.Series[0].Points[setindexX].YValues[0]);
        series2.Color = Color.Red;
        series2.ChartType = SeriesChartType.Point;
        series2.Points[0].Label = series2.Points[0].XValue.ToString("F2") + ", "
                                + series2.Points[0].YValues[0].ToString("F2");


        zoomchart.Series.Add(series);
        zoomchart.Series.Add(series2);
        zoomchart.Invalidate();

        zoomchart.ChartAreas[0].AxisX.Minimum = series.Points[0].XValue;
        zoomchart.ChartAreas[0].AxisX.Maximum = series.Points.FindMaxByValue("X").XValue;
        zoomchart.ChartAreas[0].AxisY.Minimum = series.Points.FindMinByValue().YValues[0];
        zoomchart.ChartAreas[0].AxisY.Maximum = series.Points.FindMaxByValue().YValues[0];

    }
user2587
  • 149
  • 10