-
Notifications
You must be signed in to change notification settings - Fork 204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
plot: extending the Ticker interface #581
Comments
that kind of makes sense to me. I would perhaps use this interface instead, though: type Ticker interface {
// Ticks returns Ticks in the specified range and
// according to the given font definition.
Ticks(min, max float64, font *vg.Font) []Tick
} |
do you have example plots for the rendering of your densed ticks? |
But you also need the axis width to calculate the marks. And if you only provide the font, you also need to know whether the axis is horizontal or vertical. |
I think this tries to do to much. The point of the interface is that it is simple. |
@kortschak |
It doesn't do that though. You are attempting to use the information to decide how far apart text should be on an axis, where the axis is defined by a |
This is not an option if you don't have an interactive system to design the plot, but the plot is created in a batch process. In this case aesthetics is not that important. In this case I want to display as many markers as possible without overlapping to make the chart as readable as possible. And that's very simple, you don't need a human being to do that. But you need a bit more information than is available now. But you can also specify the axis and its orientation when generating the ticker. This would work and you wouldn't have to change anything, But it undermines the ticker's ability to be independent of the axis and its orientation. |
If that's the case you can define a |
I've tried that, but it turns out to be quite messy. The current design makes it difficult to access the required dimensions. Anyway, thanks for your time. |
I'm a bit confused about why. The |
I came up with this little program that does what you want. package main
import (
"image/color"
"log"
"math/rand"
"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, err := plot.New()
if err != nil {
log.Fatalf("%+v", err)
}
p.Title.Text = "random data"
p.X.Label.Text = "x"
p.Y.Label.Text = "y"
var (
w = 10 * vg.Centimeter
h = 10 * vg.Centimeter
)
p.X.Tick.Marker = newXTicker(p.X.Tick.Marker, p, w, h)
p.Y.Tick.Marker = newYTicker(p.Y.Tick.Marker, p, w, h)
fill(p)
_ = p.Save(w, h, "foo.png")
}
type MyTicker struct {
ticker plot.Ticker
width func() vg.Length
size func(s string) vg.Length
}
func newTicker(ticker plot.Ticker, width func() vg.Length, size func(s string) vg.Length) *MyTicker {
if ticker == nil {
ticker = new(plot.DefaultTicks)
}
return &MyTicker{
ticker: ticker,
width: width,
size: size,
}
}
func newXTicker(ticker plot.Ticker, p *plot.Plot, w, h vg.Length) *MyTicker {
c := draw.New(vgimg.New(w, h))
width := func() vg.Length {
max := p.X.Norm(p.X.Max)
min := p.X.Norm(p.X.Min)
return c.X(max) - c.X(min)
}
size := p.X.Tick.Label.Font.Width
return newTicker(ticker, width, size)
}
func newYTicker(ticker plot.Ticker, p *plot.Plot, w, h vg.Length) *MyTicker {
c := draw.New(vgimg.New(w, h))
width := func() vg.Length {
max := p.Y.Norm(p.Y.Max)
min := p.Y.Norm(p.Y.Min)
return c.Y(max) - c.Y(min)
}
size := func(s string) vg.Length {
return p.Y.Tick.Label.Font.Size
}
return newTicker(ticker, width, size)
}
func (mt MyTicker) Ticks(min, max float64) []plot.Tick {
log.Printf("size= %v", mt.size("hello"))
log.Printf("width= %v", mt.width())
log.Printf("min=%v", min)
log.Printf("max=%v", max)
return mt.ticker.Ticks(min, max)
}
func fill(p *plot.Plot) {
rnd := rand.New(rand.NewSource(1))
// randomPoints returns some random x, y points
// with some interesting kind of trend.
randomPoints := func(n int) plotter.XYs {
pts := make(plotter.XYs, n)
for i := range pts {
if i == 0 {
pts[i].X = rnd.Float64()
} else {
pts[i].X = pts[i-1].X + rnd.Float64()
}
pts[i].Y = pts[i].X + 10*rnd.Float64()
}
return pts
}
s, err := plotter.NewScatter(randomPoints(15))
if err != nil {
log.Panic(err)
}
s.GlyphStyle.Color = color.RGBA{R: 255, B: 128, A: 255}
s.GlyphStyle.Radius = vg.Points(3)
p.Add(s, plotter.NewGrid())
} |
Yep, that was also my problem: I could not access the canvas. |
What about something like this: type RenderContext struct {
Axis Axis
Orientation bool
Canvas Canvas
}
type Ticker interface {
// Ticks returns Ticks in the specified range and
// according to the given font definition.
Ticks(min, max float64, rct RenderContext) []Tick
} It adds not to much clutter to the Ticker interface and it helps in the Ticker implementation because it's very easy to pass all required information to some kind of factory method. |
The issue I have with this is the impact that it would have on importing packages. While we are free to break downstream, we try to avoid it if at all possible. The benefits to be gained need to be significant if we are going to break the users of plot. At the moment, the issue that you have can be worked around without a change in API, and it's not entirely clear how the additional API would interact with the existing tickers that we provide (Lin Hanrahan and Talbot for example is agnostic to the information that the addition would provide); this suggests that the parameters for the axis and canvas should be fields in the implementations' structs. Fundamentally, the API change breaks a design decision that was made when the plot package was originally written, that the data side and the rendering side of plotting are separate entities. If you want to move forward with this, I'd like to see a more detailed proposal that explains how the system would work. |
These are my thoughts on that:
Would you agree with these conclusions? If I'm the only one who sees it that way, there's obviously no point for me to do anything about it. Which I would also be perfectly fine with. |
I've just had another look at the plot and axis code and ISTM that the canvas is not needed. This ultimately means that only the |
at this rate, it could just be: type Ticker interface {
Ticks(axis Axis) []Tick
} but there's still the issue of computing the width of the axis (for that, IIUC, one needs a |
To be honest, I don't get it! |
Sorry, you're right (cold-muddled head). I think I'd be happy with
where the length is calculated by |
what could be done to mitigate the breakage is to expose a new, different, interface that would be leveraged optionally (like is done in, e.g., e.g.: type TickerFrom interface {
TicksFrom(a Axis, w vg.Length) []Tick
}
// ...
func (a horizontalAxis) size() (h vg.Length) {
if a.Label.Text != "" { // We assume that the label isn't rotated.
h -= a.Label.Font.Extents().Descent
h += a.Label.Height(a.Label.Text)
}
var marks []Ticks
switch ticker := a.Tick.Marker.(type) {
case TickerFrom:
marks = ticker.TicksFrom(a, width)
default:
marks = a.Tick.Marker.Ticks(a.Min, a.Max)
}
if len(marks) > 0 {
if a.drawTicks() {
h += a.Tick.Length
}
h += tickLabelHeight(a.Tick.Label, marks)
}
h += a.Width / 2
h += a.Padding
return h
} and document. |
Or you simply add a new property to the Axis struct. Say What also is possible is this solution: type Ticker interface {
Ticks(min, max float64) []Tick
}
type TickerFrom interface {
Ticker
TicksFrom(min, max float64, rct *RenderContext) []Tick
}
type impl struct {
}
func (i impl) Ticks(min, max float64) []Tick {
return i.TicksFrom(min, max, nil)
}
func (impl) TicksFrom(min, max float64, rct *RenderContext) []Tick {
fmt.Println("go!")
return nil
}
func createTicks(min, max float64, ticker Ticker) []Tick {
if contextTicker, ok := ticker.(TickerFrom); ok {
return contextTicker.TicksFrom(min, max, &RenderContext{})
} else {
return ticker.Ticks(min, max)
}
} This extention also breaks no existing code, and the |
I don't really like the need to double up the methods. An alternative is to have a conditional setter that hands the axis and length to the ticker for it to work from; I'd rather not add a new type. |
So you are thinking about something like this? type impl struct {
}
func (i impl) Ticks(min, max float64) []plot.Tick {
return nil
}
func (i impl) SetAxis(axis plot.Axis, len vg.Length, orientation bool) {
}
func createTicks(min, max float64, ticker Ticker, axis plot.Axis, len vg.Length, orientation bool) []plot.Tick {
type setter interface {
SetAxis(plot.Axis, vg.Length, bool)
}
if hasSetter, ok := ticker.(setter); ok {
hasSetter.SetAxis(axis, len, orientation)
}
return ticker.Ticks(min, max)
} |
Something like that, yes. Orientation is not a bool though, maybe |
I agree! |
that's fixed (w/ #583 ) :) |
Up to now I like this the most: type Ticker interface {
Ticks(min, max float64) []plot.Tick
}
type SizedTicker interface {
Ticker
SetAxis(axis *plot.Axis, width vg.Length, orientation Orientation)
}
type impl struct {
}
var _ SizedTicker = impl{}
func (i impl) Ticks(min, max float64) []plot.Tick {
return nil
}
func (i impl) SetAxis(axis *plot.Axis, len vg.Length, orientation Orientation) {
fmt.Println("use sizes")
}
func createTicks(min, max float64, ticker Ticker, axis *plot.Axis, canvas draw.Canvas) []plot.Tick {
if needsAxis, ok := ticker.(SizedTicker); ok {
width, orientation := axis.getWidthAndOrientation(canvas)
needsAxis.SetAxis(axis, width, orientation)
}
return ticker.Ticks(min, max)
} This will not break any existing code and looks clean to me. A completely invisible method adds some kind of magic to the API. |
Here you can find a prototype implementation of this enhancement. The repo includes a package BTW: There is a rounding issue in |
I hope to get to look at this reasonably soon. |
Have you thought about extending the Ticker interface so that the font size and graphic width can be accessed? It seems a bit strange to me to create markers if you don't know the font size and how much space is available.
In my experience, you often want to have as many tick marks as possible as long as you can still read the labels, which means that they must not overlap.
To create such tick marks, however, you need to have access to the font size and the graphic size.
I have implemented this as an example to meet my requirements. Maybe this or a similar extension should be integrated.
The text was updated successfully, but these errors were encountered: