Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one set an image as or along a chart axis?

I am trying to use a colored spectrum strip as an axis for a chart. The idea is to match the color on the image with its associated wavelength along the x-axis at the bottom. The strip needs to change in size to match changes of the chart area and expand and contract sections to match scroll-zooming in the chart area.

I have tried using image annotations but as the chart area changes, the annotation dimensions remain fixed. Also, the scroll zooming that focuses in on mouse position obviously has no effect on the annotation.

The approach that came closest was using the image as a background for the chart area. This automatically scaled the image as the chart area changed but scroll-zooming has no effect on the background image. Also, it would be ideal to have the background clear so as to avoid obscuring data plot points. I can edit the image to have a large transparent section and only a colored strip at the bottom but even then, that strip could obscure lower intensity data points.

Spectrum as annotation and background: enter image description here

Annotation not scaling, background scales well: enter image description here

Both annotation and background not scaling with zooming: enter image description here

like image 937
Alex Hong Avatar asked Jan 02 '23 12:01

Alex Hong


1 Answers

This is a nice idea.

The simplest way is to draw the image in a Paint event of the Chart, maybe PrePaint.

enter image description here

Let's go to work.. We will use the DrawImage overload that allows us zooming as well as cropping. For this we need two rectangles.

The first challenge is to always get the correct target rectangle.

For this we need to convert the InnerPlotPosition from relative positions to absolute pixels.

These two functions will help:

RectangleF ChartAreaClientRectangle(Chart chart, ChartArea CA)
{
    RectangleF CAR = CA.Position.ToRectangleF();
    float pw = chart.ClientSize.Width / 100f;
    float ph = chart.ClientSize.Height / 100f;
    return new RectangleF(pw * CAR.X, ph * CAR.Y, pw * CAR.Width, ph * CAR.Height);
}

RectangleF InnerPlotPositionClientRectangle(Chart chart, ChartArea CA)
{
    RectangleF IPP = CA.InnerPlotPosition.ToRectangleF();
    RectangleF CArp = ChartAreaClientRectangle(chart, CA);

    float pw = CArp.Width / 100f;
    float ph = CArp.Height / 100f;

    return new RectangleF(CArp.X + pw * IPP.X, CArp.Y + ph * IPP.Y, 
                            pw * IPP.Width, ph * IPP.Height);
}

With these numbers setting the destination rectangle is as simple as:

Rectangle tgtR = Rectangle.Round(new RectangleF(ipr.Left, ipr.Bottom - 15, ipr.Width, 15));

You can chose a height as you like..

The next challenge is the source rectangle.

Without zooming it would simply be:

Rectangle srcR = new Rectangle( 0, 0, bmp.Width, bmp.Height);

But for zooming and panning we need to scale it; for this we can use the x-axis and the ScaleView's Minimum and Maximum values.

We calculate factors for the first and last spot on the axis:

double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);

now we get the source rectangle maybe like this:

int x  = (int)(bmp.Width * f1);
int xx = (int)(bmp.Width * f2);
Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height);

Let's put it together:

private void chart_PrePaint(object sender, ChartPaintEventArgs e)
{
    // a few short names
    Graphics g = e.ChartGraphics.Graphics;  
    ChartArea ca = chart.ChartAreas[0];
    Axis ax = ca.AxisX;

    // pixels of plot area
    RectangleF ipr = InnerPlotPositionClientRectangle(chart, ca);

    // scaled first and last position
    double f1 = ax.ScaleView.ViewMinimum / (ax.Maximum - ax.Minimum);
    double f2 = ax.ScaleView.ViewMaximum / (ax.Maximum - ax.Minimum);

    // actual drawing with the zooming overload
    using (Bitmap bmp = (Bitmap)Bitmap.FromFile(imagePath))
    {
        int x  = (int)(bmp.Width * f1);
        int xx = (int)(bmp.Width * f2);
        Rectangle srcR = new Rectangle( x, 0, xx - x, bmp.Height);
        Rectangle tgtR = Rectangle.Round(
                         new RectangleF(ipr.Left , ipr.Bottom - 15, ipr.Width, 15));
        g.DrawImage(bmp, tgtR, srcR, GraphicsUnit.Pixel);
    }
}

A few notes:

  • Of course I would recomend to use an Image resource instead of always loading from disk!

  • The Drawing will always overlay the data points and also the grids. You can either..

  • choose a different minimum to make room

  • make the image smaller

  • move it below the x-axis labels

  • make the image semi-transparent

  • make the x-axis so fat that it can hold the image strip : ax.LineWidth = 10

For the latter solution you would want to offset the y-position depending on the zoom state. Quick and dirty: int yoff = (ax.ScaleView.IsZoomed ? 12 : 5);. To avoid black stripes also make the axis Transparent or chart.BackColor..

Update:

You can also revert to using a StripLine. It can scale its BackgroundImage and you would have to create a suitable image whenever changing the scaleview, i.e. when zooming or panning. For this much of the above code would be used to create the new images. See this post for examples of adding and replacing varying NamedImage to a Chart! (The relevant portion is close to the end about the marker images!)

In fact I found that way to be the best solution and have added a second answer.

like image 99
TaW Avatar answered Jan 08 '23 06:01

TaW