gonum / plot

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

plot: glyph boxes and text handling (ascent/descent) #649

Closed sbinet closed 3 years ago

sbinet commented 3 years ago

it seems our handling of text could be improved.

consider this program:

// +build ignore

package main

import (
    "log"

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

func main() {
    const dpi = vgimg.DefaultDPI

    p, err := plot.New()
    if err != nil {
        log.Fatalf("could not create plot: %+v", err)
    }

    p.Title.Text = "Labels"
    p.X.Min = -1
    p.X.Max = +1
    p.Y.Min = -1
    p.Y.Max = +1

    labels, err := plotter.NewLabels(plotter.XYLabels{
        XYs: []plotter.XY{
            {X: -0.5, Y: +0.5},
            {X: +0.5, Y: +0.5},
            {X: +0.5, Y: -0.5},
            {X: -0.5, Y: -0.5},
        },
        Labels: []string{
            "App0\nApp1", "Bgg0", "Cqq0\nCqq1\nCqq2", "Djj0",
        },
    })
    if err != nil {
        log.Fatalf("could not creates labels plotter: %+v", err)
    }
    for i := range labels.TextStyle {
        labels.TextStyle[i].Font.Size = vg.Length(34)
    }

    p.Add(labels)
    p.Add(plotter.NewGrid())
    p.Add(plotter.NewGlyphBoxes())

    err = p.Save(15*vg.Centimeter, 15*vg.Centimeter, "labels.png")
    if err != nil {
        log.Fatalf("could save plot: %+v", err)
    }
}

running it leads to: labels

we probably still don't handle descent/ascent well, or at least, not in a coherent fashion. glyphterms_2x

it seems the glyph box of a text symbol is only considering the bounding box of that symbol. I'd argue that's not how the glyph box should be defined: as it's used to determine whether some graphic element is "on the plot", it should take into account the descent of the symbol.

it would also seem we don't have a very consistent definition of the baseline (leading to all that head room in those glyphboxes, especially for the multi-line ones)

sbinet commented 3 years ago

a perhaps better program:

// +build ignore

package main

import (
    "image/color"
    "log"

    "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() {
    const dpi = vgimg.DefaultDPI

    p, err := plot.New()
    if err != nil {
        log.Fatalf("could not create plot: %+v", err)
    }

    p.Title.Text = "Labels"
    p.X.Min = -1
    p.X.Max = +1
    p.Y.Min = -1
    p.Y.Max = +1

    labels, err := plotter.NewLabels(plotter.XYLabels{
        XYs: []plotter.XY{
            {X: -0.8 + 0.00, Y: -0.5},
            {X: -0.6 + 0.02, Y: -0.5},
            {X: -0.4 + 0.04, Y: -0.5},
            {X: -0.8 + 0.00, Y: +0.5},
            {X: -0.6 + 0.02, Y: +0.5},
            {X: -0.4 + 0.04, Y: +0.5},
            {X: +0.0 + 0.00, Y: +0},
            {X: +0.2 + 0.02, Y: +0},
            {X: +0.4 + 0.04, Y: +0},
        },
        Labels: []string{
            "Aq", "Aq", "Aq",
            "Aq\nAq", "Aq\nAq", "Aq\nAq",

            "Bg\nBg\nBg",
            "Bg\nBg\nBg",
            "Bg\nBg\nBg",
        },
    })
    if err != nil {
        log.Fatalf("could not creates labels plotter: %+v", err)
    }
    for i := range labels.TextStyle {
        sty := &labels.TextStyle[i]
        sty.Font.Size = vg.Length(34)
    }
    labels.TextStyle[0].YAlign = draw.YBottom
    labels.TextStyle[1].YAlign = draw.YCenter
    labels.TextStyle[2].YAlign = draw.YTop

    labels.TextStyle[3].YAlign = draw.YBottom
    labels.TextStyle[4].YAlign = draw.YCenter
    labels.TextStyle[5].YAlign = draw.YTop

    labels.TextStyle[6].YAlign = draw.YBottom
    labels.TextStyle[7].YAlign = draw.YCenter
    labels.TextStyle[8].YAlign = draw.YTop

    m5 := plotter.NewFunction(func(float64) float64 { return -0.5 })
    m5.LineStyle.Color = color.RGBA{R: 255, A: 255}

    l0 := plotter.NewFunction(func(float64) float64 { return 0 })
    l0.LineStyle.Color = color.RGBA{G: 255, A: 255}

    p5 := plotter.NewFunction(func(float64) float64 { return +0.5 })
    p5.LineStyle.Color = color.RGBA{B: 255, A: 255}

    p.Add(labels, m5, l0, p5)
    p.Add(plotter.NewGrid())
    p.Add(plotter.NewGlyphBoxes())

    err = p.Save(15*vg.Centimeter, 15*vg.Centimeter, "labels.png")
    if err != nil {
        log.Fatalf("could save plot: %+v", err)
    }
}

labels

sbinet commented 3 years ago

and a possible fix:

diff --git a/vg/draw/text.go b/vg/draw/text.go
index e19ff3e..7f28db6 100644
--- a/vg/draw/text.go
+++ b/vg/draw/text.go
@@ -74,16 +74,17 @@ func (sty TextStyle) Height(txt string) vg.Length {
                return vg.Length(0)
        }
        e := sty.Font.Extents()
-       return e.Height*vg.Length(nl-1) + e.Ascent - e.Descent
+       return e.Height * vg.Length(nl)
 }

 // Rectangle returns a rectangle giving the bounds of
 // this text assuming that it is drawn at (0, 0).
 func (sty TextStyle) Rectangle(txt string) vg.Rectangle {
+       e := sty.Font.Extents()
        w := sty.Width(txt)
        h := sty.Height(txt)
        xoff := vg.Length(sty.XAlign) * w
-       yoff := vg.Length(sty.YAlign) * h
+       yoff := vg.Length(sty.YAlign)*h + e.Descent
        // lower left corner
        p1 := rotatePoint(sty.Rotation, vg.Point{X: xoff, Y: yoff})
        // upper left corner

labels

(there's still this extra head room at the top of the glyphbox. it's the space of the descent of the (possible) line above)

@kortschak what do you think?