gonum / plot

A repository for plotting and visualizing data
BSD 3-Clause "New" or "Revised" License
2.72k stars 202 forks source link

plot: Plot renders outside of image bounds #761

Closed mlange-42 closed 1 year ago

mlange-42 commented 1 year ago

What are you trying to do?

Just trying to make a simple line plot. However, depending on the axis limits, the rendered plot does not fit onto the image and exceeds its bounds.

What did you do?

Minimal reproducing example:

package main

import (
    "math/rand"

    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
)

func main() {
    p := plot.New()

    p.Legend = plot.NewLegend()

    length := 70
    xy := make(plotter.XYs, length)
    for i := 0; i < length; i++ {
        xy[i] = plotter.XY{X: float64(i), Y: rand.Float64()}
    }

    lines, err := plotter.NewLine(xy)
    if err != nil {
        panic(err)
    }

    p.Add(lines)
    p.Legend.Add("Legend entry", lines)

    p.Save(600, 400, "test.png")
}

What did you expect to happen?

The plot nicely fills the image.

What actually happened?

The plot is partially outside the image bounds:

test

What version of Go and Gonum/plot are you using?

go version go1.20 windows/amd64 plot vrsion v0.12.0

Does this issue reproduce with the current master?

Yes.

sbinet commented 1 year ago

if you replace the last point to, say, (69, 0.5) and add a grid (plotter.NewGrid()), you'll see the last point is correctly drawn.

see:

package main

import (
    "math/rand"

    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
)

func main() {
    p := plot.New()

    p.Legend = plot.NewLegend()

    length := 70
    xy := make(plotter.XYs, length)
    for i := 0; i < length; i++ {
        y := rand.Float64()
        if i == length-1 {
            y = 0.5
        }
        xy[i] = plotter.XY{X: float64(i), Y: y}
    }

    lines, err := plotter.NewLine(xy)
    if err != nil {
        panic(err)
    }
    p.Add(lines, plotter.NewGrid())
    p.Legend.Add("Legend entry", lines)

    p.Save(600, 400, "test.png")
}

test

ie: the plotted range is [0, 69], not [0, 70].

mlange-42 commented 1 year ago

But shouldn't the x axis line as well as the line series end inside the image, not touching the right edge? So, at least it looks broken IMO.

sbinet commented 1 year ago

you can add a right margin, if you want.

package main

import (
    "math/rand"
    "os"

    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/vg"
    "gonum.org/v1/plot/vg/draw"
    "gonum.org/v1/plot/vg/vgimg"
)

func main() {
    p := plot.New()

    p.Legend = plot.NewLegend()

    length := 70
    xy := make(plotter.XYs, length)
    for i := 0; i < length; i++ {
        y := rand.Float64()
        if i == length-1 {
            y = 0.5
        }
        xy[i] = plotter.XY{X: float64(i), Y: y}
    }

    lines, err := plotter.NewLine(xy)
    if err != nil {
        panic(err)
    }
    p.Add(lines, plotter.NewGrid())
    p.Legend.Add("Legend entry", lines)

    tp := draw.Tiles{
        Cols:     1,
        Rows:     1,
        PadRight: 2 * vg.Centimeter,
    }
    cnv := vgimg.PngCanvas{vgimg.New(600, 400)}

    p.Draw(tp.At(draw.New(cnv), 0, 0))

    f, err := os.Create("border.png")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    _, err = cnv.WriteTo(f)
    if err != nil {
        panic(err)
    }

    err = f.Close()
    if err != nil {
        panic(err)
    }
}

border

mlange-42 commented 1 year ago

Ok, thanks! Will go this route. First I thought "shouldn't that better be fixed in the plot library"? But I guess it is intentional that the image is filled to the last pixel. May be a bit cumbersome for just quickly plotting something, but it is ok in my use case.

sbinet commented 1 year ago

on second thoughts, perhaps the plotter.Line isn't properly accounting for the line's width in its plot.DataRanger implementation.

currently, plotter.Line will return (0, 69, ymin, ymax) in your example. perhaps it should return (0-dr, 69+dr, ymin-dr, ymax+dr), where dr=0.5*line.LineStyle.Width

also, perhaps the "feeling of a cut out plot" is coming from the "missing" (minor) tick for 70. while the former issue is easily fixable, the latter is way more complicated.

sbinet commented 1 year ago

actually, that's probably the plot.GlyphBoxer, not the plot.DataRanger. :)

sbinet commented 1 year ago

here's what it would look like:

what do you think ?

mlange-42 commented 1 year ago

This would definitely be helpful, particularly when using a wide line style.