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
|
||||
}
|
||||
|
||||
// UnixTimeTicks is suitable for axes representing time values.
|
||||
// UnixTimeTicks expects values in Unix time seconds.
|
||||
type UnixTimeTicks struct {
|
||||
// UnixTimeIn returns a time conversion function for the given location.
|
||||
func UnixTimeIn(loc *time.Location) func(t float64) time.Time {
|
||||
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.
|
||||
// If nil, DefaultTicks will be used.
|
||||
Ticker Ticker
|
||||
|
|
@ -472,27 +481,33 @@ type UnixTimeTicks struct {
|
|||
// Format is the textual representation of the time value.
|
||||
// If empty, time.RFC3339 will be used
|
||||
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.
|
||||
func (utt UnixTimeTicks) Ticks(min, max float64) []Tick {
|
||||
if utt.Ticker == nil {
|
||||
utt.Ticker = DefaultTicks{}
|
||||
func (t TimeTicks) Ticks(min, max float64) []Tick {
|
||||
if t.Ticker == nil {
|
||||
t.Ticker = DefaultTicks{}
|
||||
}
|
||||
if utt.Format == "" {
|
||||
utt.Format = time.RFC3339
|
||||
if t.Format == "" {
|
||||
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 {
|
||||
tick := &ticks[i]
|
||||
if tick.Label == "" {
|
||||
continue
|
||||
}
|
||||
t := time.Unix(int64(tick.Value), 0)
|
||||
tick.Label = t.Format(utt.Format)
|
||||
tick.Label = t.Time(tick.Value).Format(t.Format)
|
||||
}
|
||||
return ticks
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ type HeatMap struct {
|
|||
// NewHeatMap creates as new heat map plotter for the given data,
|
||||
// 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
|
||||
// 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 {
|
||||
var min, max float64
|
||||
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.
|
||||
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()
|
||||
if len(pal) == 0 {
|
||||
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