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
}
GlyphBoxes implements the GlyphBoxer interface.
package plotter
import (
	"fmt"
	"image/color"
	"math"
	"sort"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/tools/bezier"
	"gonum.org/v1/plot/vg"
	"gonum.org/v1/plot/vg/draw"
)
type Sankey struct {
	Color color.Color
	StockBarWidth vg.Length
	LineStyle draw.LineStyle
	TextStyle draw.TextStyle
	flows []Flow
	FlowStyle func(group string) (color.Color, draw.LineStyle)
	StockStyle func(label string, category int) (lbl string, ts draw.TextStyle, xOff, yOff vg.Length, c color.Color, ls draw.LineStyle)
	stocks map[int]map[string]*stock
}
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
}
type stock struct {
	receptorValue, sourceValue float64
	label    string
	category int
	order int
	min float64
	max float64
}
type Flow struct {
	SourceLabel, ReceptorLabel string
	SourceCategory, ReceptorCategory int
	Value float64
	Group string
}
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 {
		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)
		}
		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)
		}
		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,
			}
		}
		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
}
func (s *Sankey) Plot(c draw.Canvas, plt *plot.Plot) {
	trCat, trVal := plt.Transforms(&c)
	sourceFlowPlaceholder := make(map[*stock]float64, len(s.flows))
	receptorFlowPlaceholder := make(map[*stock]float64, len(s.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 + sourceFlowPlaceholder[startStock])
		valEndLow := trVal(endStock.min + receptorFlowPlaceholder[endStock])
		valStartHigh := trVal(startStock.min + sourceFlowPlaceholder[startStock] + f.Value)
		valEndHigh := trVal(endStock.min + receptorFlowPlaceholder[endStock] + f.Value)
		sourceFlowPlaceholder[startStock] += f.Value
		receptorFlowPlaceholder[endStock] += 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)
		if color != nil {
			poly := c.ClipPolygonX(append(ptsLow, ptsHigh...))
			c.FillPolygon(color, poly)
		}
		outline := c.ClipLinesX(ptsLow)
		c.StrokeLines(lineStyle, outline...)
		outline = c.ClipLinesX(ptsHigh)
		c.StrokeLines(lineStyle, outline...)
	}
	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)
		pts := []vg.Point{
			{catMin, valMin},
			{catMin, valMax},
			{catMax, valMax},
			{catMax, valMin},
		}
		if color != nil {
			c.FillPolygon(color, pts)  
		}
		txtPt := vg.Point{X: (catMin+catMax)/2 + xOff, Y: (valMin+valMax)/2 + yOff}
		c.FillText(textStyle, txtPt, label)
		pts = []vg.Point{
			{catMin, valMin},
			{catMax, valMin},
		}
		c.StrokeLines(lineStyle, pts)
		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)
	}
}
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
}
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]))
}
func (s *Sankey) setStockRange(stocks *[]*stock) {
	var cat int
	var min float64
	for _, stk := range *stocks {
		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
	}
}
func (s *Sankey) bezier(begin, end vg.Point) []vg.Point {
	const directionOffsetFrac = 0.3
	inPts := []vg.Point{
		begin,
		{X: begin.X + (end.X-begin.X)*directionOffsetFrac, Y: begin.Y},
		{X: begin.X + (end.X-begin.X)*(1-directionOffsetFrac), Y: end.Y},
		end,
	}
	curve := bezier.New(inPts...)
	const nPoints = 20
	outPts := make([]vg.Point, nPoints)
	curve.Curve(outPts)
	return outPts
}
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
}