mirror of https://github.com/etcd-io/dbtester.git
vendor: update 'gonum/plot'
This commit is contained in:
parent
78617dc062
commit
ed57d76c6f
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright ©2013 The bíogo Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package bezier implements 2D Bézier curve calculation.
|
||||||
|
package bezier
|
||||||
|
|
||||||
|
import "github.com/gonum/plot/vg"
|
||||||
|
|
||||||
|
type point struct {
|
||||||
|
Point, Control vg.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curve implements Bezier curve calculation according to the algorithm of Robert D. Miller.
|
||||||
|
//
|
||||||
|
// Graphics Gems 5, 'Quick and Simple Bézier Curve Drawing', pages 206-209.
|
||||||
|
type Curve []point
|
||||||
|
|
||||||
|
// NewCurve returns a Curve initialized with the control points in cp.
|
||||||
|
func New(cp ...vg.Point) Curve {
|
||||||
|
if len(cp) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := make(Curve, len(cp))
|
||||||
|
for i, p := range cp {
|
||||||
|
c[i].Point = p
|
||||||
|
}
|
||||||
|
|
||||||
|
var w vg.Length
|
||||||
|
for i, p := range c {
|
||||||
|
if i == 0 {
|
||||||
|
w = 1
|
||||||
|
} else if i == 1 {
|
||||||
|
w = vg.Length(len(c)) - 1
|
||||||
|
} else {
|
||||||
|
w *= vg.Length(len(c)-i) / vg.Length(i)
|
||||||
|
}
|
||||||
|
c[i].Control.X = p.Point.X * w
|
||||||
|
c[i].Control.Y = p.Point.Y * w
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point returns the point at t along the curve, where 0 ≤ t ≤ 1.
|
||||||
|
func (c Curve) Point(t float64) vg.Point {
|
||||||
|
c[0].Point = c[0].Control
|
||||||
|
u := t
|
||||||
|
for i, p := range c[1:] {
|
||||||
|
c[i+1].Point = vg.Point{p.Control.X * vg.Length(u), p.Control.Y * vg.Length(u)}
|
||||||
|
u *= t
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
t1 = 1 - t
|
||||||
|
tt = t1
|
||||||
|
)
|
||||||
|
p := c[len(c)-1].Point
|
||||||
|
for i := len(c) - 2; i >= 0; i-- {
|
||||||
|
p.X += c[i].Point.X * vg.Length(tt)
|
||||||
|
p.Y += c[i].Point.Y * vg.Length(tt)
|
||||||
|
tt *= t1
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curve returns a slice of vg.Point, p, filled with points along the Bézier curve described by c.
|
||||||
|
// If the length of p is less than 2, the curve points are undefined. The length of p is not
|
||||||
|
// altered by the call.
|
||||||
|
func (c Curve) Curve(p []vg.Point) []vg.Point {
|
||||||
|
for i, nf := 0, float64(len(p)-1); i < len(p); i++ {
|
||||||
|
p[i] = c.Point(float64(i) / nf)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
@ -462,9 +462,18 @@ func (ts ConstantTicks) Ticks(float64, float64) []Tick {
|
||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnixTimeTicks is suitable for axes representing time values.
|
// UnixTimeIn returns a time conversion function for the given location.
|
||||||
// UnixTimeTicks expects values in Unix time seconds.
|
func UnixTimeIn(loc *time.Location) func(t float64) time.Time {
|
||||||
type UnixTimeTicks struct {
|
return func(t float64) time.Time {
|
||||||
|
return time.Unix(int64(t), 0).In(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTCUnixTime is the default time conversion for TimeTicks.
|
||||||
|
var UTCUnixTime = UnixTimeIn(time.UTC)
|
||||||
|
|
||||||
|
// TimeTicks is suitable for axes representing time values.
|
||||||
|
type TimeTicks struct {
|
||||||
// Ticker is used to generate a set of ticks.
|
// Ticker is used to generate a set of ticks.
|
||||||
// If nil, DefaultTicks will be used.
|
// If nil, DefaultTicks will be used.
|
||||||
Ticker Ticker
|
Ticker Ticker
|
||||||
|
|
@ -472,27 +481,33 @@ type UnixTimeTicks struct {
|
||||||
// Format is the textual representation of the time value.
|
// Format is the textual representation of the time value.
|
||||||
// If empty, time.RFC3339 will be used
|
// If empty, time.RFC3339 will be used
|
||||||
Format string
|
Format string
|
||||||
|
|
||||||
|
// Time takes a float64 value and converts it into a time.Time.
|
||||||
|
// If nil, UTCUnixTime is used.
|
||||||
|
Time func(t float64) time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Ticker = UnixTimeTicks{}
|
var _ Ticker = TimeTicks{}
|
||||||
|
|
||||||
// Ticks implements plot.Ticker.
|
// Ticks implements plot.Ticker.
|
||||||
func (utt UnixTimeTicks) Ticks(min, max float64) []Tick {
|
func (t TimeTicks) Ticks(min, max float64) []Tick {
|
||||||
if utt.Ticker == nil {
|
if t.Ticker == nil {
|
||||||
utt.Ticker = DefaultTicks{}
|
t.Ticker = DefaultTicks{}
|
||||||
}
|
}
|
||||||
if utt.Format == "" {
|
if t.Format == "" {
|
||||||
utt.Format = time.RFC3339
|
t.Format = time.RFC3339
|
||||||
|
}
|
||||||
|
if t.Time == nil {
|
||||||
|
t.Time = UTCUnixTime
|
||||||
}
|
}
|
||||||
|
|
||||||
ticks := utt.Ticker.Ticks(min, max)
|
ticks := t.Ticker.Ticks(min, max)
|
||||||
for i := range ticks {
|
for i := range ticks {
|
||||||
tick := &ticks[i]
|
tick := &ticks[i]
|
||||||
if tick.Label == "" {
|
if tick.Label == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t := time.Unix(int64(tick.Value), 0)
|
tick.Label = t.Time(tick.Value).Format(t.Format)
|
||||||
tick.Label = t.Format(utt.Format)
|
|
||||||
}
|
}
|
||||||
return ticks
|
return ticks
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ type HeatMap struct {
|
||||||
// NewHeatMap creates as new heat map plotter for the given data,
|
// NewHeatMap creates as new heat map plotter for the given data,
|
||||||
// using the provided palette. If g has Min and Max methods that return
|
// using the provided palette. If g has Min and Max methods that return
|
||||||
// a float, those returned values are used to set the respective HeatMap
|
// a float, those returned values are used to set the respective HeatMap
|
||||||
// fields.
|
// fields. If the returned HeatMap is used when Min is greater than or
|
||||||
|
// equal to Max, the Plot method will panic.
|
||||||
func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
|
func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
|
||||||
var min, max float64
|
var min, max float64
|
||||||
type minMaxer interface {
|
type minMaxer interface {
|
||||||
|
|
@ -92,6 +93,9 @@ func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
|
||||||
|
|
||||||
// Plot implements the Plot method of the plot.Plotter interface.
|
// Plot implements the Plot method of the plot.Plotter interface.
|
||||||
func (h *HeatMap) Plot(c draw.Canvas, plt *plot.Plot) {
|
func (h *HeatMap) Plot(c draw.Canvas, plt *plot.Plot) {
|
||||||
|
if h.Min >= h.Max {
|
||||||
|
panic("heatmap: non-positive Z range")
|
||||||
|
}
|
||||||
pal := h.Palette.Colors()
|
pal := h.Palette.Colors()
|
||||||
if len(pal) == 0 {
|
if len(pal) == 0 {
|
||||||
panic("heatmap: empty palette")
|
panic("heatmap: empty palette")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,477 @@
|
||||||
|
// Copyright ©2016 The gonum Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package plotter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/biogo/graphics/bezier"
|
||||||
|
"github.com/gonum/plot"
|
||||||
|
"github.com/gonum/plot/vg"
|
||||||
|
"github.com/gonum/plot/vg/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Sankey diagram presents stock and flow data as rectangles representing
|
||||||
|
// the amount of each stock and lines between the stocks representing the
|
||||||
|
// amount of each flow.
|
||||||
|
type Sankey struct {
|
||||||
|
// Color specifies the default fill
|
||||||
|
// colors for the stocks and flows. If Color is not nil,
|
||||||
|
// each stock and flow is rendered filled with Color,
|
||||||
|
// otherwise no fill is performed. Colors can be
|
||||||
|
// modified for individual stocks and flows.
|
||||||
|
Color color.Color
|
||||||
|
|
||||||
|
// StockBarWidth is the widths of the bars representing
|
||||||
|
// the stocks. The default value is 15% larger than the
|
||||||
|
// height of the stock label text.
|
||||||
|
StockBarWidth vg.Length
|
||||||
|
|
||||||
|
// LineStyle specifies the default border
|
||||||
|
// line style for the stocks and flows. Styles can be
|
||||||
|
// modified for individual stocks and flows.
|
||||||
|
LineStyle draw.LineStyle
|
||||||
|
|
||||||
|
// TextStyle specifies the default stock label
|
||||||
|
// text style. Styles can be modified for
|
||||||
|
// individual stocks.
|
||||||
|
TextStyle draw.TextStyle
|
||||||
|
|
||||||
|
flows []Flow
|
||||||
|
|
||||||
|
// FlowStyle is a function that specifies the
|
||||||
|
// background color and border line style of the
|
||||||
|
// flow based on its group name. The default
|
||||||
|
// function uses the default Color and LineStyle
|
||||||
|
// specified above for all groups.
|
||||||
|
FlowStyle func(group string) (color.Color, draw.LineStyle)
|
||||||
|
|
||||||
|
// StockStyle is a function that specifies, for a stock
|
||||||
|
// identified by its label and category, the label text
|
||||||
|
// to be printed on the plot (lbl), the style of the text (ts),
|
||||||
|
// the horizontal and vertical offsets for printing the text (xOff and yOff),
|
||||||
|
// the color of the fill for the bar representing the stock (c),
|
||||||
|
// and the style of the outline of the bar representing the stock (ls).
|
||||||
|
// The default function uses the default TextStyle, color and LineStyle
|
||||||
|
// specified above for all stocks; zero horizontal and vertical offsets;
|
||||||
|
// and the stock label as the text to be printed on the plot.
|
||||||
|
StockStyle func(label string, category int) (lbl string, ts draw.TextStyle, xOff, yOff vg.Length, c color.Color, ls draw.LineStyle)
|
||||||
|
|
||||||
|
// stocks arranges the stocks by category.
|
||||||
|
// The first key is the category and the seond
|
||||||
|
// key is the label.
|
||||||
|
stocks map[int]map[string]*stock
|
||||||
|
}
|
||||||
|
|
||||||
|
// StockRange returns the minimum and maximum value on the value axis
|
||||||
|
// for the stock with the specified label and category.
|
||||||
|
func (s *Sankey) StockRange(label string, category int) (min, max float64, err error) {
|
||||||
|
stk, ok := s.stocks[category][label]
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, fmt.Errorf("plotter: sankey diagram does not contain stock with label=%s and category=%d", label, category)
|
||||||
|
}
|
||||||
|
return stk.min, stk.max, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stock represents the amount of a stock and its plotting order.
|
||||||
|
type stock struct {
|
||||||
|
// receptorValue and sourceValue are the totals of the values
|
||||||
|
// of flows coming into and going out of this stock, respectively.
|
||||||
|
receptorValue, sourceValue float64
|
||||||
|
|
||||||
|
// label is the label of this stock, and category represents
|
||||||
|
// its placement on the category axis. Together they make up a
|
||||||
|
// unique identifier.
|
||||||
|
label string
|
||||||
|
category int
|
||||||
|
|
||||||
|
// order is the plotting order of this stock compared
|
||||||
|
// to other stocks in the same category.
|
||||||
|
order int
|
||||||
|
|
||||||
|
// min represents the beginning of the plotting location
|
||||||
|
// on the value axis.
|
||||||
|
min float64
|
||||||
|
|
||||||
|
// max is min plus the larger of receptorValue and sourceValue.
|
||||||
|
max float64
|
||||||
|
|
||||||
|
// sourceFlowPlaceholder and receptorFlowPlaceholder track
|
||||||
|
// the current plotting location during
|
||||||
|
// the plotting process.
|
||||||
|
sourceFlowPlaceholder, receptorFlowPlaceholder float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Flow represents the amount of an entity flowing between two stocks.
|
||||||
|
type Flow struct {
|
||||||
|
// SourceLabel and ReceptorLabel are the labels
|
||||||
|
// of the stocks that originate and receive the flow,
|
||||||
|
// respectively.
|
||||||
|
SourceLabel, ReceptorLabel string
|
||||||
|
|
||||||
|
// SourceCategory and ReceptorCategory define
|
||||||
|
// the locations on the category axis of the stocks that
|
||||||
|
// originate and receive the flow, respectively. The
|
||||||
|
// SourceCategory must be a lower number than
|
||||||
|
// the ReceptorCategory.
|
||||||
|
SourceCategory, ReceptorCategory int
|
||||||
|
|
||||||
|
// Value represents the magnitute of the flow.
|
||||||
|
// It must be greater than or equal to zero.
|
||||||
|
Value float64
|
||||||
|
|
||||||
|
// Group specifies the group that a flow belongs
|
||||||
|
// to. It is used in assigning styles to groups
|
||||||
|
// and creating legends.
|
||||||
|
Group string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSankey creates a new Sankey diagram with the specified
|
||||||
|
// flows and stocks.
|
||||||
|
func NewSankey(flows ...Flow) (*Sankey, error) {
|
||||||
|
var s Sankey
|
||||||
|
|
||||||
|
s.stocks = make(map[int]map[string]*stock)
|
||||||
|
|
||||||
|
s.flows = flows
|
||||||
|
for i, f := range flows {
|
||||||
|
// Here we make sure the stock categories are in the proper order.
|
||||||
|
if f.SourceCategory >= f.ReceptorCategory {
|
||||||
|
return nil, fmt.Errorf("plotter: Flow %d SourceCategory (%d) >= ReceptorCategory (%d)", i, f.SourceCategory, f.ReceptorCategory)
|
||||||
|
}
|
||||||
|
if f.Value < 0 {
|
||||||
|
return nil, fmt.Errorf("plotter: Flow %d value (%g) < 0", i, f.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we initialize the stock holders.
|
||||||
|
if _, ok := s.stocks[f.SourceCategory]; !ok {
|
||||||
|
s.stocks[f.SourceCategory] = make(map[string]*stock)
|
||||||
|
}
|
||||||
|
if _, ok := s.stocks[f.ReceptorCategory]; !ok {
|
||||||
|
s.stocks[f.ReceptorCategory] = make(map[string]*stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we figure out the plotting order of the stocks.
|
||||||
|
if _, ok := s.stocks[f.SourceCategory][f.SourceLabel]; !ok {
|
||||||
|
s.stocks[f.SourceCategory][f.SourceLabel] = &stock{
|
||||||
|
order: len(s.stocks[f.SourceCategory]),
|
||||||
|
label: f.SourceLabel,
|
||||||
|
category: f.SourceCategory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := s.stocks[f.ReceptorCategory][f.ReceptorLabel]; !ok {
|
||||||
|
s.stocks[f.ReceptorCategory][f.ReceptorLabel] = &stock{
|
||||||
|
order: len(s.stocks[f.ReceptorCategory]),
|
||||||
|
label: f.ReceptorLabel,
|
||||||
|
category: f.ReceptorCategory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we add the current value to the total value of the stocks
|
||||||
|
s.stocks[f.SourceCategory][f.SourceLabel].sourceValue += f.Value
|
||||||
|
s.stocks[f.ReceptorCategory][f.ReceptorLabel].receptorValue += f.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
s.LineStyle = DefaultLineStyle
|
||||||
|
|
||||||
|
fnt, err := vg.MakeFont(DefaultFont, DefaultFontSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.TextStyle = draw.TextStyle{
|
||||||
|
Font: fnt,
|
||||||
|
Rotation: math.Pi / 2,
|
||||||
|
XAlign: draw.XCenter,
|
||||||
|
YAlign: draw.YCenter,
|
||||||
|
}
|
||||||
|
s.StockBarWidth = s.TextStyle.Font.Extents().Height * 1.15
|
||||||
|
|
||||||
|
s.FlowStyle = func(_ string) (color.Color, draw.LineStyle) {
|
||||||
|
return s.Color, s.LineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
s.StockStyle = func(label string, category int) (string, draw.TextStyle, vg.Length, vg.Length, color.Color, draw.LineStyle) {
|
||||||
|
return label, s.TextStyle, 0, 0, s.Color, s.LineStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
stocks := s.stockList()
|
||||||
|
s.setStockRange(&stocks)
|
||||||
|
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plot implements the plot.Plotter interface.
|
||||||
|
func (s *Sankey) Plot(c draw.Canvas, plt *plot.Plot) {
|
||||||
|
trCat, trVal := plt.Transforms(&c)
|
||||||
|
|
||||||
|
// Here we draw the flows.
|
||||||
|
for _, f := range s.flows {
|
||||||
|
startStock := s.stocks[f.SourceCategory][f.SourceLabel]
|
||||||
|
endStock := s.stocks[f.ReceptorCategory][f.ReceptorLabel]
|
||||||
|
catStart := trCat(float64(f.SourceCategory)) + s.StockBarWidth/2
|
||||||
|
catEnd := trCat(float64(f.ReceptorCategory)) - s.StockBarWidth/2
|
||||||
|
valStartLow := trVal(startStock.min + startStock.sourceFlowPlaceholder)
|
||||||
|
valEndLow := trVal(endStock.min + endStock.receptorFlowPlaceholder)
|
||||||
|
valStartHigh := trVal(startStock.min + startStock.sourceFlowPlaceholder + f.Value)
|
||||||
|
valEndHigh := trVal(endStock.min + endStock.receptorFlowPlaceholder + f.Value)
|
||||||
|
startStock.sourceFlowPlaceholder += f.Value
|
||||||
|
endStock.receptorFlowPlaceholder += f.Value
|
||||||
|
|
||||||
|
ptsLow := s.bezier(
|
||||||
|
vg.Point{X: catStart, Y: valStartLow},
|
||||||
|
vg.Point{X: catEnd, Y: valEndLow},
|
||||||
|
)
|
||||||
|
ptsHigh := s.bezier(
|
||||||
|
vg.Point{X: catEnd, Y: valEndHigh},
|
||||||
|
vg.Point{X: catStart, Y: valStartHigh},
|
||||||
|
)
|
||||||
|
|
||||||
|
color, lineStyle := s.FlowStyle(f.Group)
|
||||||
|
|
||||||
|
// Here we fill the flow polygons.
|
||||||
|
if color != nil {
|
||||||
|
poly := c.ClipPolygonX(append(ptsLow, ptsHigh...))
|
||||||
|
c.FillPolygon(color, poly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we draw the flow edges.
|
||||||
|
outline := c.ClipLinesX(ptsLow)
|
||||||
|
c.StrokeLines(lineStyle, outline...)
|
||||||
|
outline = c.ClipLinesX(ptsHigh)
|
||||||
|
c.StrokeLines(lineStyle, outline...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we draw the stocks.
|
||||||
|
for _, stk := range s.stockList() {
|
||||||
|
catLoc := trCat(float64(stk.category))
|
||||||
|
if !c.ContainsX(catLoc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
catMin, catMax := catLoc-s.StockBarWidth/2, catLoc+s.StockBarWidth/2
|
||||||
|
valMin, valMax := trVal(stk.min), trVal(stk.max)
|
||||||
|
|
||||||
|
label, textStyle, xOff, yOff, color, lineStyle := s.StockStyle(stk.label, stk.category)
|
||||||
|
|
||||||
|
// Here we fill the stock bars.
|
||||||
|
pts := []vg.Point{
|
||||||
|
{catMin, valMin},
|
||||||
|
{catMin, valMax},
|
||||||
|
{catMax, valMax},
|
||||||
|
{catMax, valMin},
|
||||||
|
}
|
||||||
|
if s.Color != nil {
|
||||||
|
// poly := c.ClipPolygonX(pts) // This causes half of the bar to disappear. Is there a best practice here?
|
||||||
|
c.FillPolygon(color, pts) // poly)
|
||||||
|
}
|
||||||
|
txtPt := vg.Point{X: (catMin+catMax)/2 + xOff, Y: (valMin+valMax)/2 + yOff}
|
||||||
|
c.FillText(textStyle, txtPt, label)
|
||||||
|
|
||||||
|
// Here we draw the bottom edge.
|
||||||
|
pts = []vg.Point{
|
||||||
|
{catMin, valMin},
|
||||||
|
{catMax, valMin},
|
||||||
|
}
|
||||||
|
c.StrokeLines(lineStyle, pts)
|
||||||
|
|
||||||
|
// Here we draw the top edge plus vertical edges where there are
|
||||||
|
// no flows connected.
|
||||||
|
pts = []vg.Point{
|
||||||
|
{catMin, valMax},
|
||||||
|
{catMax, valMax},
|
||||||
|
}
|
||||||
|
if stk.receptorValue < stk.sourceValue {
|
||||||
|
y := trVal(stk.max - (stk.sourceValue - stk.receptorValue))
|
||||||
|
pts = append([]vg.Point{{catMin, y}}, pts...)
|
||||||
|
} else if stk.sourceValue < stk.receptorValue {
|
||||||
|
y := trVal(stk.max - (stk.receptorValue - stk.sourceValue))
|
||||||
|
pts = append(pts, vg.Point{X: catMax, Y: y})
|
||||||
|
}
|
||||||
|
c.StrokeLines(lineStyle, pts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stockList returns a sorted list of the stocks in the diagram.
|
||||||
|
func (s *Sankey) stockList() []*stock {
|
||||||
|
var stocks []*stock
|
||||||
|
for _, ss := range s.stocks {
|
||||||
|
for _, sss := range ss {
|
||||||
|
stocks = append(stocks, sss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(stockSorter(stocks))
|
||||||
|
return stocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// stockSorter is a wrapper for a list of *stocks that implements
|
||||||
|
// sort.Interface.
|
||||||
|
type stockSorter []*stock
|
||||||
|
|
||||||
|
func (s stockSorter) Len() int { return len(s) }
|
||||||
|
func (s stockSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s stockSorter) Less(i, j int) bool {
|
||||||
|
if s[i].category != s[j].category {
|
||||||
|
return s[i].category < s[j].category
|
||||||
|
}
|
||||||
|
if s[i].order != s[j].order {
|
||||||
|
return s[i].order < s[j].order
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("plotter: can't sort stocks:\n%+v\n%+v", s[i], s[j]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// setStockRange sets the minimum and maximum values of the stock plotting locations.
|
||||||
|
func (s *Sankey) setStockRange(stocks *[]*stock) {
|
||||||
|
var cat int
|
||||||
|
var min float64
|
||||||
|
for _, stk := range *stocks {
|
||||||
|
stk.sourceFlowPlaceholder = 0
|
||||||
|
stk.receptorFlowPlaceholder = 0
|
||||||
|
if stk.category != cat {
|
||||||
|
min = 0
|
||||||
|
}
|
||||||
|
cat = stk.category
|
||||||
|
stk.min = min
|
||||||
|
if stk.sourceValue > stk.receptorValue {
|
||||||
|
stk.max = stk.min + stk.sourceValue
|
||||||
|
} else {
|
||||||
|
stk.max = stk.min + stk.receptorValue
|
||||||
|
}
|
||||||
|
min = stk.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bezier creates a bezier curve between the begin and end points.
|
||||||
|
func (s *Sankey) bezier(begin, end vg.Point) []vg.Point {
|
||||||
|
// directionOffsetFrac is the fraction of the distance between begin.X and
|
||||||
|
// end.X for the bezier control points.
|
||||||
|
const directionOffsetFrac = 0.3
|
||||||
|
inPts := []vg.Point{
|
||||||
|
begin,
|
||||||
|
vg.Point{X: begin.X + (end.X-begin.X)*directionOffsetFrac, Y: begin.Y},
|
||||||
|
vg.Point{X: begin.X + (end.X-begin.X)*(1-directionOffsetFrac), Y: end.Y},
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
curve := bezier.New(inPts...)
|
||||||
|
|
||||||
|
// nPoints is the number of points for bezier interpolation.
|
||||||
|
const nPoints = 20
|
||||||
|
outPts := make([]vg.Point, nPoints)
|
||||||
|
curve.Curve(outPts)
|
||||||
|
return outPts
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataRange implements the plot.DataRanger interface.
|
||||||
|
func (s *Sankey) DataRange() (xmin, xmax, ymin, ymax float64) {
|
||||||
|
catMin := math.Inf(1)
|
||||||
|
catMax := math.Inf(-1)
|
||||||
|
for cat := range s.stocks {
|
||||||
|
c := float64(cat)
|
||||||
|
catMin = math.Min(catMin, c)
|
||||||
|
catMax = math.Max(catMax, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
stocks := s.stockList()
|
||||||
|
valMin := math.Inf(1)
|
||||||
|
valMax := math.Inf(-1)
|
||||||
|
for _, stk := range stocks {
|
||||||
|
valMin = math.Min(valMin, stk.min)
|
||||||
|
valMax = math.Max(valMax, stk.max)
|
||||||
|
}
|
||||||
|
return catMin, catMax, valMin, valMax
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlyphBoxes implements the GlyphBoxer interface.
|
||||||
|
func (s *Sankey) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
|
||||||
|
stocks := s.stockList()
|
||||||
|
boxes := make([]plot.GlyphBox, 0, len(s.flows)+len(stocks))
|
||||||
|
|
||||||
|
for _, stk := range stocks {
|
||||||
|
b1 := plot.GlyphBox{
|
||||||
|
X: plt.X.Norm(float64(stk.category)),
|
||||||
|
Y: plt.Y.Norm((stk.min + stk.max) / 2),
|
||||||
|
Rectangle: vg.Rectangle{
|
||||||
|
Min: vg.Point{X: -s.StockBarWidth / 2},
|
||||||
|
Max: vg.Point{X: s.StockBarWidth / 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
label, textStyle, xOff, yOff, _, _ := s.StockStyle(stk.label, stk.category)
|
||||||
|
rect := textStyle.Rectangle(label)
|
||||||
|
rect.Min.X += xOff
|
||||||
|
rect.Max.X += xOff
|
||||||
|
rect.Min.Y += yOff
|
||||||
|
rect.Max.Y += yOff
|
||||||
|
b2 := plot.GlyphBox{
|
||||||
|
X: plt.X.Norm(float64(stk.category)),
|
||||||
|
Y: plt.Y.Norm((stk.min + stk.max) / 2),
|
||||||
|
Rectangle: rect,
|
||||||
|
}
|
||||||
|
boxes = append(boxes, b1, b2)
|
||||||
|
}
|
||||||
|
return boxes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnailers creates a group of objects that can be used to
|
||||||
|
// add legend entries for the different flow groups in this
|
||||||
|
// diagram, as well as the flow group labels that correspond to them.
|
||||||
|
func (s *Sankey) Thumbnailers() (legendLabels []string, thumbnailers []plot.Thumbnailer) {
|
||||||
|
type empty struct{}
|
||||||
|
flowGroups := make(map[string]empty)
|
||||||
|
for _, f := range s.flows {
|
||||||
|
flowGroups[f.Group] = empty{}
|
||||||
|
}
|
||||||
|
legendLabels = make([]string, len(flowGroups))
|
||||||
|
thumbnailers = make([]plot.Thumbnailer, len(flowGroups))
|
||||||
|
i := 0
|
||||||
|
for g := range flowGroups {
|
||||||
|
legendLabels[i] = g
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sort.Strings(legendLabels)
|
||||||
|
|
||||||
|
for i, g := range legendLabels {
|
||||||
|
var thmb sankeyFlowThumbnailer
|
||||||
|
thmb.Color, thmb.LineStyle = s.FlowStyle(g)
|
||||||
|
thumbnailers[i] = plot.Thumbnailer(thmb)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// sankeyFlowThumbnailer implements the Thumbnailer interface
|
||||||
|
// for Sankey flow groups.
|
||||||
|
type sankeyFlowThumbnailer struct {
|
||||||
|
draw.LineStyle
|
||||||
|
color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail fulfills the plot.Thumbnailer interface.
|
||||||
|
func (t sankeyFlowThumbnailer) Thumbnail(c *draw.Canvas) {
|
||||||
|
// Here we draw the fill.
|
||||||
|
pts := []vg.Point{
|
||||||
|
{c.Min.X, c.Min.Y},
|
||||||
|
{c.Min.X, c.Max.Y},
|
||||||
|
{c.Max.X, c.Max.Y},
|
||||||
|
{c.Max.X, c.Min.Y},
|
||||||
|
}
|
||||||
|
poly := c.ClipPolygonY(pts)
|
||||||
|
c.FillPolygon(t.Color, poly)
|
||||||
|
|
||||||
|
// Here we draw the upper border.
|
||||||
|
pts = []vg.Point{
|
||||||
|
{c.Min.X, c.Max.Y},
|
||||||
|
{c.Max.X, c.Max.Y},
|
||||||
|
}
|
||||||
|
outline := c.ClipLinesY(pts)
|
||||||
|
c.StrokeLines(t.LineStyle, outline...)
|
||||||
|
|
||||||
|
// Here we draw the lower border.
|
||||||
|
pts = []vg.Point{
|
||||||
|
{c.Min.X, c.Min.Y},
|
||||||
|
{c.Max.X, c.Min.Y},
|
||||||
|
}
|
||||||
|
outline = c.ClipLinesY(pts)
|
||||||
|
c.StrokeLines(t.LineStyle, outline...)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue