1// +build cairo
2
3package png
4
5import (
6	"bytes"
7	"fmt"
8	"image/color"
9	"io/ioutil"
10	"math"
11	"net/http"
12	"os"
13	"sort"
14	"strings"
15	"time"
16
17	"github.com/go-graphite/carbonapi/expr/helper"
18	"github.com/go-graphite/carbonapi/expr/types"
19	"github.com/go-graphite/carbonapi/pkg/parser"
20	pb "github.com/go-graphite/protocol/carbonapi_v3_pb"
21
22	"bitbucket.org/tebeka/strftime"
23	"github.com/evmar/gocairo/cairo"
24)
25
26const HaveGraphSupport = true
27
28type HAlign int
29
30const (
31	HAlignLeft   HAlign = 1
32	HAlignCenter        = 2
33	HAlignRight         = 4
34)
35
36type VAlign int
37
38const (
39	VAlignTop      VAlign = 8
40	VAlignCenter          = 16
41	VAlignBottom          = 32
42	VAlignBaseline        = 64
43)
44
45type YCoordSide int
46
47const (
48	YCoordSideLeft  YCoordSide = 1
49	YCoordSideRight            = 2
50	YCoordSideNone             = 3
51)
52
53type TimeUnit int32
54
55const (
56	Second TimeUnit = 1
57	Minute          = 60
58	Hour            = 60 * Minute
59	Day             = 24 * Hour
60)
61
62type unitPrefix struct {
63	prefix string
64	size   uint64
65}
66
67const (
68	unitSystemBinary = "binary"
69	unitSystemSI     = "si"
70)
71
72var unitSystems = map[string][]unitPrefix{
73	unitSystemBinary: {
74		{"Pi", 1125899906842624}, // 1024^5
75		{"Ti", 1099511627776},    // 1024^4
76		{"Gi", 1073741824},       // 1024^3
77		{"Mi", 1048576},          // 1024^2
78		{"Ki", 1024},
79	},
80	unitSystemSI: {
81		{"P", 1000000000000000}, // 1000^5
82		{"T", 1000000000000},    // 1000^4
83		{"G", 1000000000},       // 1000^3
84		{"M", 1000000},          // 1000^2
85		{"K", 1000},
86	},
87}
88
89type xAxisStruct struct {
90	seconds       float64
91	minorGridUnit TimeUnit
92	minorGridStep float64
93	majorGridUnit TimeUnit
94	majorGridStep int64
95	labelUnit     TimeUnit
96	labelStep     int64
97	format        string
98	maxInterval   int64
99}
100
101var xAxisConfigs = []xAxisStruct{
102	{
103		seconds:       0.00,
104		minorGridUnit: Second,
105		minorGridStep: 5,
106		majorGridUnit: Minute,
107		majorGridStep: 1,
108		labelUnit:     Second,
109		labelStep:     5,
110		format:        "%H:%M:%S",
111		maxInterval:   10 * Minute,
112	},
113	{
114		seconds:       0.07,
115		minorGridUnit: Second,
116		minorGridStep: 10,
117		majorGridUnit: Minute,
118		majorGridStep: 1,
119		labelUnit:     Second,
120		labelStep:     10,
121		format:        "%H:%M:%S",
122		maxInterval:   20 * Minute,
123	},
124	{
125		seconds:       0.14,
126		minorGridUnit: Second,
127		minorGridStep: 15,
128		majorGridUnit: Minute,
129		majorGridStep: 1,
130		labelUnit:     Second,
131		labelStep:     15,
132		format:        "%H:%M:%S",
133		maxInterval:   30 * Minute,
134	},
135	{
136		seconds:       0.27,
137		minorGridUnit: Second,
138		minorGridStep: 30,
139		majorGridUnit: Minute,
140		majorGridStep: 2,
141		labelUnit:     Minute,
142		labelStep:     1,
143		format:        "%H:%M",
144		maxInterval:   2 * Hour,
145	},
146	{
147		seconds:       0.5,
148		minorGridUnit: Minute,
149		minorGridStep: 1,
150		majorGridUnit: Minute,
151		majorGridStep: 2,
152		labelUnit:     Minute,
153		labelStep:     1,
154		format:        "%H:%M",
155		maxInterval:   2 * Hour,
156	},
157	{
158		seconds:       1.2,
159		minorGridUnit: Minute,
160		minorGridStep: 1,
161		majorGridUnit: Minute,
162		majorGridStep: 4,
163		labelUnit:     Minute,
164		labelStep:     2,
165		format:        "%H:%M",
166		maxInterval:   3 * Hour,
167	},
168	{
169		seconds:       2,
170		minorGridUnit: Minute,
171		minorGridStep: 1,
172		majorGridUnit: Minute,
173		majorGridStep: 10,
174		labelUnit:     Minute,
175		labelStep:     5,
176		format:        "%H:%M",
177		maxInterval:   6 * Hour,
178	},
179	{
180		seconds:       5,
181		minorGridUnit: Minute,
182		minorGridStep: 2,
183		majorGridUnit: Minute,
184		majorGridStep: 10,
185		labelUnit:     Minute,
186		labelStep:     10,
187		format:        "%H:%M",
188		maxInterval:   12 * Hour,
189	},
190	{
191		seconds:       10,
192		minorGridUnit: Minute,
193		minorGridStep: 5,
194		majorGridUnit: Minute,
195		majorGridStep: 20,
196		labelUnit:     Minute,
197		labelStep:     20,
198		format:        "%H:%M",
199		maxInterval:   Day,
200	},
201	{
202		seconds:       30,
203		minorGridUnit: Minute,
204		minorGridStep: 10,
205		majorGridUnit: Hour,
206		majorGridStep: 1,
207		labelUnit:     Hour,
208		labelStep:     1,
209		format:        "%H:%M",
210		maxInterval:   2 * Day,
211	},
212	{
213		seconds:       60,
214		minorGridUnit: Minute,
215		minorGridStep: 30,
216		majorGridUnit: Hour,
217		majorGridStep: 2,
218		labelUnit:     Hour,
219		labelStep:     2,
220		format:        "%H:%M",
221		maxInterval:   2 * Day,
222	},
223	{
224		seconds:       100,
225		minorGridUnit: Hour,
226		minorGridStep: 2,
227		majorGridUnit: Hour,
228		majorGridStep: 4,
229		labelUnit:     Hour,
230		labelStep:     4,
231		format:        "%a %H:%M",
232		maxInterval:   6 * Day,
233	},
234	{
235		seconds:       255,
236		minorGridUnit: Hour,
237		minorGridStep: 6,
238		majorGridUnit: Hour,
239		majorGridStep: 12,
240		labelUnit:     Hour,
241		labelStep:     12,
242		format:        "%a %H:%M",
243		maxInterval:   10 * Day,
244	},
245	{
246		seconds:       600,
247		minorGridUnit: Hour,
248		minorGridStep: 6,
249		majorGridUnit: Day,
250		majorGridStep: 1,
251		labelUnit:     Day,
252		labelStep:     1,
253		format:        "%m/%d",
254		maxInterval:   14 * Day,
255	},
256	{
257		seconds:       1000,
258		minorGridUnit: Hour,
259		minorGridStep: 12,
260		majorGridUnit: Day,
261		majorGridStep: 1,
262		labelUnit:     Day,
263		labelStep:     1,
264		format:        "%m/%d",
265		maxInterval:   365 * Day,
266	},
267	{
268		seconds:       2000,
269		minorGridUnit: Day,
270		minorGridStep: 1,
271		majorGridUnit: Day,
272		majorGridStep: 2,
273		labelUnit:     Day,
274		labelStep:     2,
275		format:        "%m/%d",
276		maxInterval:   365 * Day,
277	},
278	{
279		seconds:       4000,
280		minorGridUnit: Day,
281		minorGridStep: 2,
282		majorGridUnit: Day,
283		majorGridStep: 4,
284		labelUnit:     Day,
285		labelStep:     4,
286		format:        "%m/%d",
287		maxInterval:   365 * Day,
288	},
289	{
290		seconds:       8000,
291		minorGridUnit: Day,
292		minorGridStep: 3.5,
293		majorGridUnit: Day,
294		majorGridStep: 7,
295		labelUnit:     Day,
296		labelStep:     7,
297		format:        "%m/%d",
298		maxInterval:   365 * Day,
299	},
300	{
301		seconds:       16000,
302		minorGridUnit: Day,
303		minorGridStep: 7,
304		majorGridUnit: Day,
305		majorGridStep: 14,
306		labelUnit:     Day,
307		labelStep:     14,
308		format:        "%m/%d",
309		maxInterval:   365 * Day,
310	},
311	{
312		seconds:       32000,
313		minorGridUnit: Day,
314		minorGridStep: 15,
315		majorGridUnit: Day,
316		majorGridStep: 30,
317		labelUnit:     Day,
318		labelStep:     30,
319		format:        "%m/%d",
320		maxInterval:   365 * Day,
321	},
322	{
323		seconds:       64000,
324		minorGridUnit: Day,
325		minorGridStep: 30,
326		majorGridUnit: Day,
327		majorGridStep: 60,
328		labelUnit:     Day,
329		labelStep:     60,
330		format:        "%m/%d %Y",
331		maxInterval:   365 * Day,
332	},
333	{
334		seconds:       100000,
335		minorGridUnit: Day,
336		minorGridStep: 60,
337		majorGridUnit: Day,
338		majorGridStep: 120,
339		labelUnit:     Day,
340		labelStep:     120,
341		format:        "%m/%d %Y",
342		maxInterval:   365 * Day,
343	},
344	{
345		seconds:       120000,
346		minorGridUnit: Day,
347		minorGridStep: 120,
348		majorGridUnit: Day,
349		majorGridStep: 240,
350		labelUnit:     Day,
351		labelStep:     240,
352		format:        "%m/%d %Y",
353		maxInterval:   365 * Day,
354	},
355}
356
357// We accept values fractionally outside of nominal limits, so that
358// rounding errors don't cause weird effects. Since our goal is to
359// create plots, and the maximum resolution of the plots is likely to
360// be less than 10000 pixels, errors smaller than this size shouldn't
361// create any visible effects.
362const floatEpsilon = 0.00000000001
363
364func getCairoFontItalic(s FontSlant) cairo.FontSlant {
365	if s == FontSlantItalic {
366		return cairo.FontSlantItalic
367	}
368	return cairo.FontSlantNormal
369}
370
371func getCairoFontWeight(weight FontWeight) cairo.FontWeight {
372	if weight == FontWeightBold {
373		return cairo.FontWeightBold
374	}
375
376	return cairo.FontWeightNormal
377}
378
379type Area struct {
380	xmin float64
381	xmax float64
382	ymin float64
383	ymax float64
384}
385
386type Params struct {
387	pixelRatio float64
388	width      float64
389	height     float64
390	margin     int
391	logBase    float64
392	fgColor    color.RGBA
393	bgColor    color.RGBA
394	majorLine  color.RGBA
395	minorLine  color.RGBA
396	fontName   string
397	fontSize   float64
398	fontBold   cairo.FontWeight
399	fontItalic cairo.FontSlant
400
401	graphOnly   bool
402	hideLegend  bool
403	hideGrid    bool
404	hideAxes    bool
405	hideYAxis   bool
406	hideXAxis   bool
407	yAxisSide   YAxisSide
408	title       string
409	vtitle      string
410	vtitleRight string
411	tz          *time.Location
412	timeRange   int64
413	startTime   int64
414	endTime     int64
415
416	lineMode       LineMode
417	areaMode       AreaMode
418	areaAlpha      float64
419	pieMode        PieMode
420	colorList      []string
421	lineWidth      float64
422	connectedLimit int
423	hasStack       bool
424
425	yMin   float64
426	yMax   float64
427	xMin   float64
428	xMax   float64
429	yStep  float64
430	xStep  float64
431	minorY int
432
433	yTop           float64
434	yBottom        float64
435	ySpan          float64
436	graphHeight    float64
437	graphWidth     float64
438	yScaleFactor   float64
439	yUnitSystem    string
440	yDivisors      []float64
441	yLabelValues   []float64
442	yLabels        []string
443	yLabelWidth    float64
444	xScaleFactor   float64
445	xFormat        string
446	xLabelStep     int64
447	xMinorGridStep int64
448	xMajorGridStep int64
449
450	minorGridLineColor string
451	majorGridLineColor string
452
453	yTopL         float64
454	yBottomL      float64
455	yLabelValuesL []float64
456	yLabelsL      []string
457	yLabelWidthL  float64
458	yTopR         float64
459	yBottomR      float64
460	yLabelValuesR []float64
461	yLabelsR      []string
462	yLabelWidthR  float64
463	yStepL        float64
464	yStepR        float64
465	ySpanL        float64
466	ySpanR        float64
467	yScaleFactorL float64
468	yScaleFactorR float64
469
470	yMaxLeft    float64
471	yLimitLeft  float64
472	yMaxRight   float64
473	yLimitRight float64
474	yMinLeft    float64
475	yMinRight   float64
476
477	dataLeft  []*types.MetricData
478	dataRight []*types.MetricData
479
480	rightWidth  float64
481	rightDashed bool
482	rightColor  string
483	leftWidth   float64
484	leftDashed  bool
485	leftColor   string
486
487	area        Area
488	isPng       bool // TODO: png and svg use the same code
489	fontExtents cairo.FontExtents
490
491	uniqueLegend   bool
492	secondYAxis    bool
493	drawNullAsZero bool
494	drawAsInfinite bool
495
496	xConf xAxisStruct
497}
498
499type cairoBackend int
500
501const (
502	cairoPNG cairoBackend = iota
503	cairoSVG
504)
505
506func Description() map[string]types.FunctionDescription {
507	return map[string]types.FunctionDescription{
508		"color": {
509			Name: "color",
510			Params: []types.FunctionParam{
511				{
512					Name:     "seriesList",
513					Required: true,
514					Type:     types.SeriesList,
515				},
516				{
517					Name:     "theColor",
518					Required: true,
519					Type:     types.String,
520				},
521			},
522			Module:      "graphite.render.functions",
523			Description: "Assigns the given color to the seriesList\n\nExample:\n\n.. code-block:: none\n\n  &target=color(collectd.hostname.cpu.0.user, 'green')\n  &target=color(collectd.hostname.cpu.0.system, 'ff0000')\n  &target=color(collectd.hostname.cpu.0.idle, 'gray')\n  &target=color(collectd.hostname.cpu.0.idle, '6464ffaa')",
524			Function:    "color(seriesList, theColor)",
525			Group:       "Graph",
526		},
527		"stacked": {
528			Name: "stacked",
529			Params: []types.FunctionParam{
530				{
531					Name:     "seriesList",
532					Required: true,
533					Type:     types.SeriesList,
534				},
535				{
536					Name: "stack",
537					Type: types.String,
538				},
539			},
540			Module:      "graphite.render.functions",
541			Description: "Takes one metric or a wildcard seriesList and change them so they are\nstacked. This is a way of stacking just a couple of metrics without having\nto use the stacked area mode (that stacks everything). By means of this a mixed\nstacked and non stacked graph can be made\n\nIt can also take an optional argument with a name of the stack, in case there is\nmore than one, e.g. for input and output metrics.\n\nExample:\n\n.. code-block:: none\n\n  &target=stacked(company.server.application01.ifconfig.TXPackets, 'tx')",
542			Function:    "stacked(seriesLists, stackName='__DEFAULT__')",
543			Group:       "Graph",
544		},
545		"areaBetween": {
546			Name: "areaBetween",
547			Params: []types.FunctionParam{
548				{
549					Name:     "seriesList",
550					Required: true,
551					Type:     types.SeriesList,
552				},
553			},
554			Module:      "graphite.render.functions",
555			Description: "Draws the vertical area in between the two series in seriesList. Useful for\nvisualizing a range such as the minimum and maximum latency for a service.\n\nareaBetween expects **exactly one argument** that results in exactly two series\n(see example below). The order of the lower and higher values series does not\nmatter. The visualization only works when used in conjunction with\n``areaMode=stacked``.\n\nMost likely use case is to provide a band within which another metric should\nmove. In such case applying an ``alpha()``, as in the second example, gives\nbest visual results.\n\nExample:\n\n.. code-block:: none\n\n  &target=areaBetween(service.latency.{min,max})&areaMode=stacked\n\n  &target=alpha(areaBetween(service.latency.{min,max}),0.3)&areaMode=stacked\n\nIf for instance, you need to build a seriesList, you should use the ``group``\nfunction, like so:\n\n.. code-block:: none\n\n  &target=areaBetween(group(minSeries(a.*.min),maxSeries(a.*.max)))",
556			Function:    "areaBetween(seriesList)",
557			Group:       "Graph",
558		},
559		"alpha": {
560			Name: "alpha",
561			Params: []types.FunctionParam{
562				{
563					Name:     "seriesList",
564					Required: true,
565					Type:     types.SeriesList,
566				},
567				{
568					Name:     "alpha",
569					Required: true,
570					Type:     types.Float,
571				},
572			},
573			Module:      "graphite.render.functions",
574			Description: "Assigns the given alpha transparency setting to the series. Takes a float value between 0 and 1.",
575			Function:    "alpha(seriesList, alpha)",
576			Group:       "Graph",
577		},
578		"dashed": {
579			Name: "dashed",
580			Params: []types.FunctionParam{
581				{
582					Name:     "seriesList",
583					Required: true,
584					Type:     types.SeriesList,
585				},
586				{
587					Default: types.NewSuggestion(5),
588					Name:    "dashLength",
589					Type:    types.Integer,
590				},
591			},
592			Module:      "graphite.render.functions",
593			Description: "Takes one metric or a wildcard seriesList, followed by a float F.\n\nDraw the selected metrics with a dotted line with segments of length F\nIf omitted, the default length of the segments is 5.0\n\nExample:\n\n.. code-block:: none\n\n  &target=dashed(server01.instance01.memory.free,2.5)",
594			Function:    "dashed(seriesList, dashLength=5)",
595			Group:       "Graph",
596		},
597		"drawAsInfinite": {
598			Name: "drawAsInfinite",
599			Params: []types.FunctionParam{
600				{
601					Name:     "seriesList",
602					Required: true,
603					Type:     types.SeriesList,
604				},
605			},
606			Module:      "graphite.render.functions",
607			Description: "Takes one metric or a wildcard seriesList.\nIf the value is zero, draw the line at 0.  If the value is above zero, draw\nthe line at infinity. If the value is null or less than zero, do not draw\nthe line.\n\nUseful for displaying on/off metrics, such as exit codes. (0 = success,\nanything else = failure.)\n\nExample:\n\n.. code-block:: none\n\n  drawAsInfinite(Testing.script.exitCode)",
608			Function:    "drawAsInfinite(seriesList)",
609			Group:       "Graph",
610		},
611		"secondYAxis": {
612			Name: "secondYAxis",
613			Params: []types.FunctionParam{
614				{
615					Name:     "seriesList",
616					Required: true,
617					Type:     types.SeriesList,
618				},
619			},
620			Module:      "graphite.render.functions",
621			Description: "Graph the series on the secondary Y axis.",
622			Function:    "secondYAxis(seriesList)",
623			Group:       "Graph",
624		},
625		"lineWidth": {
626			Name: "lineWidth",
627			Params: []types.FunctionParam{
628				{
629					Name:     "seriesList",
630					Required: true,
631					Type:     types.SeriesList,
632				},
633				{
634					Name:     "width",
635					Required: true,
636					Type:     types.Float,
637				},
638			},
639			Module:      "graphite.render.functions",
640			Description: "Takes one metric or a wildcard seriesList, followed by a float F.\n\nDraw the selected metrics with a line width of F, overriding the default\nvalue of 1, or the &lineWidth=X.X parameter.\n\nUseful for highlighting a single metric out of many, or having multiple\nline widths in one graph.\n\nExample:\n\n.. code-block:: none\n\n  &target=lineWidth(server01.instance01.memory.free,5)",
641			Function:    "lineWidth(seriesList, width)",
642			Group:       "Graph",
643		},
644		// TODO: This function doesn't depend on cairo, should be moved out
645		"threshold": {
646			Name: "threshold",
647			Params: []types.FunctionParam{
648				{
649					Name:     "value",
650					Required: true,
651					Type:     types.Float,
652				},
653				{
654					Name: "label",
655					Type: types.String,
656				},
657				{
658					Name: "color",
659					Type: types.String,
660				},
661			},
662			Module:      "graphite.render.functions",
663			Description: "Takes a float F, followed by a label (in double quotes) and a color.\n(See ``bgcolor`` in the render\\_api_ for valid color names & formats.)\n\nDraws a horizontal line at value F across the graph.\n\nExample:\n\n.. code-block:: none\n\n  &target=threshold(123.456, \"omgwtfbbq\", \"red\")",
664			Function:    "threshold(value, label=None, color=None)",
665			Group:       "Graph",
666		},
667	}
668}
669
670// TODO(civil): Split this into several separate functions.
671func EvalExprGraph(e parser.Expr, from, until int64, values map[parser.MetricRequest][]*types.MetricData) ([]*types.MetricData, error) {
672
673	switch e.Target() {
674
675	case "color": // color(seriesList, theColor)
676		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
677		if err != nil {
678			return nil, err
679		}
680
681		color, err := e.GetStringArg(1) // get color
682		if err != nil {
683			return nil, err
684		}
685
686		var results []*types.MetricData
687
688		for _, a := range arg {
689			r := *a
690			r.Color = color
691			results = append(results, &r)
692		}
693
694		return results, nil
695
696	case "stacked": // stacked(seriesList, stackname="__DEFAULT__")
697		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
698		if err != nil {
699			return nil, err
700		}
701
702		stackName, err := e.GetStringNamedOrPosArgDefault("stackname", 1, types.DefaultStackName)
703		if err != nil {
704			return nil, err
705		}
706
707		var results []*types.MetricData
708
709		for _, a := range arg {
710			r := *a
711			r.Stacked = true
712			r.StackName = stackName
713			results = append(results, &r)
714		}
715
716		return results, nil
717
718	case "areaBetween":
719		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
720		if err != nil {
721			return nil, err
722		}
723
724		if len(arg) != 2 {
725			return nil, fmt.Errorf("areaBetween needs exactly two arguments (%d given)", len(arg))
726		}
727
728		name := fmt.Sprintf("%s(%s)", e.Target(), e.RawArgs())
729
730		lower := *arg[0]
731		lower.Stacked = true
732		lower.StackName = types.DefaultStackName
733		lower.Invisible = true
734		lower.Name = name
735
736		upper := *arg[1]
737		upper.Stacked = true
738		upper.StackName = types.DefaultStackName
739		upper.Name = name
740
741		vals := make([]float64, len(upper.Values))
742
743		for i, v := range upper.Values {
744			vals[i] = v - lower.Values[i]
745		}
746
747		upper.Values = vals
748
749		return []*types.MetricData{&lower, &upper}, nil
750
751	case "alpha": // alpha(seriesList, theAlpha)
752		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
753		if err != nil {
754			return nil, err
755		}
756
757		alpha, err := e.GetFloatArg(1)
758		if err != nil {
759			return nil, err
760		}
761
762		var results []*types.MetricData
763
764		for _, a := range arg {
765			r := *a
766			r.Alpha = alpha
767			r.HasAlpha = true
768			results = append(results, &r)
769		}
770
771		return results, nil
772
773	case "dashed", "drawAsInfinite", "secondYAxis":
774		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
775		if err != nil {
776			return nil, err
777		}
778
779		var results []*types.MetricData
780
781		for _, a := range arg {
782			r := *a
783			r.Name = fmt.Sprintf("%s(%s)", e.Target(), a.Name)
784
785			switch e.Target() {
786			case "dashed":
787				d, err := e.GetFloatArgDefault(1, 2.5)
788				if err != nil {
789					return nil, err
790				}
791				r.Dashed = d
792			case "drawAsInfinite":
793				r.DrawAsInfinite = true
794			case "secondYAxis":
795				r.SecondYAxis = true
796			}
797
798			results = append(results, &r)
799		}
800		return results, nil
801
802	case "lineWidth": // lineWidth(seriesList, width)
803		arg, err := helper.GetSeriesArg(e.Args()[0], from, until, values)
804		if err != nil {
805			return nil, err
806		}
807
808		width, err := e.GetFloatArg(1)
809		if err != nil {
810			return nil, err
811		}
812
813		var results []*types.MetricData
814
815		for _, a := range arg {
816			r := *a
817			r.LineWidth = width
818			r.HasLineWidth = true
819			results = append(results, &r)
820		}
821
822		return results, nil
823
824	case "threshold": // threshold(value, label=None, color=None)
825		// TODO: This function doesn't depend on cairo, should be moved out
826		// XXX does not match graphite's signature
827		// BUG(nnuss): the signature *does* match but there is an edge case because of named argument handling if you use it *just* wrong:
828		//			   threshold(value, "gold", label="Aurum")
829		//			   will result in:
830		//			   value = value
831		//			   label = "Aurum" (by named argument)
832		//			   color = "" (by default as len(positionalArgs) == 2 and there is no named 'color' arg)
833
834		value, err := e.GetFloatArg(0)
835
836		if err != nil {
837			return nil, err
838		}
839
840		name, err := e.GetStringNamedOrPosArgDefault("label", 1, fmt.Sprintf("%g", value))
841		if err != nil {
842			return nil, err
843		}
844
845		color, err := e.GetStringNamedOrPosArgDefault("color", 2, "")
846		if err != nil {
847			return nil, err
848		}
849
850		newValues := []float64{value, value}
851		stepTime := until - from
852		stopTime := from + stepTime*int64(len(newValues))
853		p := types.MetricData{
854			FetchResponse: pb.FetchResponse{
855				Name:              name,
856				StartTime:         from,
857				StopTime:          stopTime,
858				StepTime:          stepTime,
859				Values:            newValues,
860				ConsolidationFunc: "average",
861			},
862			Tags:         map[string]string{"name": name},
863			GraphOptions: types.GraphOptions{Color: color},
864		}
865
866		return []*types.MetricData{&p}, nil
867
868	}
869
870	return nil, helper.ErrUnknownFunction(e.Target())
871}
872
873func MarshalSVG(params PictureParams, results []*types.MetricData) []byte {
874	return marshalCairo(params, results, cairoSVG)
875}
876
877func MarshalPNG(params PictureParams, results []*types.MetricData) []byte {
878	return marshalCairo(params, results, cairoPNG)
879}
880
881func MarshalSVGRequest(r *http.Request, results []*types.MetricData, templateName string) []byte {
882	return marshalCairo(GetPictureParamsWithTemplate(r, templateName, results), results, cairoSVG)
883}
884
885func MarshalPNGRequest(r *http.Request, results []*types.MetricData, templateName string) []byte {
886	return marshalCairo(GetPictureParamsWithTemplate(r, templateName, results), results, cairoPNG)
887}
888
889func marshalCairo(p PictureParams, results []*types.MetricData, backend cairoBackend) []byte {
890	var params = Params{
891		pixelRatio:     p.PixelRatio,
892		width:          p.Width,
893		height:         p.Height,
894		margin:         p.Margin,
895		logBase:        p.LogBase,
896		fgColor:        string2RGBA(p.FgColor),
897		bgColor:        string2RGBA(p.BgColor),
898		majorLine:      string2RGBA(p.MajorLine),
899		minorLine:      string2RGBA(p.MinorLine),
900		fontName:       p.FontName,
901		fontSize:       p.FontSize,
902		fontBold:       getCairoFontWeight(p.FontBold),
903		fontItalic:     getCairoFontItalic(p.FontItalic),
904		graphOnly:      p.GraphOnly,
905		hideLegend:     p.HideLegend,
906		hideGrid:       p.HideGrid,
907		hideAxes:       p.HideAxes,
908		hideYAxis:      p.HideYAxis,
909		hideXAxis:      p.HideXAxis,
910		yAxisSide:      p.YAxisSide,
911		connectedLimit: p.ConnectedLimit,
912		lineMode:       p.LineMode,
913		areaMode:       p.AreaMode,
914		areaAlpha:      p.AreaAlpha,
915		pieMode:        p.PieMode,
916		lineWidth:      p.LineWidth,
917
918		rightWidth:  p.RightWidth,
919		rightDashed: p.RightDashed,
920		rightColor:  p.RightColor,
921
922		leftWidth:  p.LeftWidth,
923		leftDashed: p.LeftDashed,
924		leftColor:  p.LeftColor,
925
926		title:       p.Title,
927		vtitle:      p.Vtitle,
928		vtitleRight: p.VtitleRight,
929		tz:          p.Tz,
930
931		colorList: p.ColorList,
932		isPng:     true,
933
934		majorGridLineColor: p.MajorGridLineColor,
935		minorGridLineColor: p.MinorGridLineColor,
936
937		uniqueLegend:   p.UniqueLegend,
938		drawNullAsZero: p.DrawNullAsZero,
939		drawAsInfinite: p.DrawAsInfinite,
940		yMin:           p.YMin,
941		yMax:           p.YMax,
942		yStep:          p.YStep,
943		xMin:           p.XMin,
944		xMax:           p.XMax,
945		xStep:          p.XStep,
946		xFormat:        p.XFormat,
947		minorY:         p.MinorY,
948
949		yMinLeft:    p.YMinLeft,
950		yMinRight:   p.YMinRight,
951		yMaxLeft:    p.YMaxLeft,
952		yMaxRight:   p.YMaxRight,
953		yStepL:      p.YStepL,
954		yStepR:      p.YStepR,
955		yLimitLeft:  p.YLimitLeft,
956		yLimitRight: p.YLimitRight,
957
958		yUnitSystem: p.YUnitSystem,
959		yDivisors:   p.YDivisors,
960	}
961
962	margin := float64(params.margin)
963	params.area.xmin = margin + 10
964	params.area.xmax = params.width - margin
965	params.area.ymin = margin
966	params.area.ymax = params.height - margin
967
968	var surface *cairo.Surface
969	var tmpfile *os.File
970	switch backend {
971	case cairoSVG:
972		var err error
973		tmpfile, err = ioutil.TempFile("/dev/shm", "cairosvg")
974		if err != nil {
975			return nil
976		}
977		defer os.Remove(tmpfile.Name())
978		s := svgSurfaceCreate(tmpfile.Name(), params.width, params.height, params.pixelRatio)
979		surface = s.Surface
980	case cairoPNG:
981		s := imageSurfaceCreate(cairo.FormatARGB32, params.width, params.height, params.pixelRatio)
982		surface = s.Surface
983	}
984	cr := createContext(surface, params.pixelRatio)
985
986	// Setting font parameters
987
988	fontOpts := cairo.FontOptionsCreate()
989	fontOpts.SetAntialias(cairo.AntialiasNone)
990	cr.context.SetFontOptions(fontOpts)
991
992	setColor(cr, params.bgColor)
993	drawRectangle(cr, &params, 0, 0, params.width, params.height, true)
994
995	drawGraph(cr, &params, results)
996
997	surface.Flush()
998
999	var b []byte
1000
1001	switch backend {
1002	case cairoPNG:
1003		var buf bytes.Buffer
1004		surface.WriteToPNG(&buf)
1005		surface.Finish()
1006		b = buf.Bytes()
1007	case cairoSVG:
1008		surface.Finish()
1009		b, _ = ioutil.ReadFile(tmpfile.Name())
1010		// NOTE(dgryski): This is the dumbest thing ever, but needed
1011		// for compatibility.  I'm not doing the rest of the svg
1012		// munging that graphite does.
1013		// We could speed this up with Index(`pt"`) and overwriting the
1014		// `t` twice
1015		b = bytes.Replace(b, []byte(`pt"`), []byte(`px"`), 2)
1016	}
1017
1018	return b
1019}
1020
1021func drawGraph(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1022	params.secondYAxis = false
1023	minNumberOfPoints := int64(0)
1024	maxNumberOfPoints := int64(0)
1025
1026	if len(results) > 0 {
1027		params.startTime = results[0].StartTime
1028		params.endTime = results[0].StopTime
1029		minNumberOfPoints = int64(len(results[0].Values))
1030		maxNumberOfPoints = minNumberOfPoints
1031		for _, res := range results {
1032			tmp := res.StartTime
1033			if params.startTime > tmp {
1034				params.startTime = tmp
1035			}
1036			tmp = res.StopTime
1037			if params.endTime > tmp {
1038				params.endTime = tmp
1039			}
1040
1041			tmp = int64(len(res.Values))
1042			if tmp < minNumberOfPoints {
1043				minNumberOfPoints = tmp
1044			}
1045			if tmp > maxNumberOfPoints {
1046				maxNumberOfPoints = tmp
1047			}
1048
1049		}
1050		params.timeRange = params.endTime - params.startTime
1051	}
1052
1053	if params.timeRange <= 0 {
1054		x := params.width / 2.0
1055		y := params.height / 2.0
1056		setColor(cr, string2RGBA("red"))
1057		fontSize := math.Log(params.width * params.height)
1058		setFont(cr, params, fontSize)
1059		drawText(cr, params, "No Data", x, y, HAlignCenter, VAlignTop, 0)
1060
1061		return
1062	}
1063
1064	for _, res := range results {
1065		if res.SecondYAxis {
1066			params.dataRight = append(params.dataRight, res)
1067		} else {
1068			params.dataLeft = append(params.dataLeft, res)
1069		}
1070	}
1071
1072	if len(params.dataRight) > 0 {
1073		params.secondYAxis = true
1074		params.yAxisSide = YAxisSideLeft
1075	}
1076
1077	if params.graphOnly {
1078		params.hideLegend = true
1079		params.hideGrid = true
1080		params.hideAxes = true
1081		params.hideYAxis = true
1082		params.area.xmin = 0
1083		params.area.xmax = params.width
1084		params.area.ymin = 0
1085		params.area.ymax = params.height
1086	}
1087
1088	if params.yAxisSide == YAxisSideRight {
1089		params.margin = int(params.width)
1090	}
1091
1092	if params.lineMode == LineModeSlope && minNumberOfPoints == 1 {
1093		params.lineMode = LineModeStaircase
1094	}
1095
1096	var colorsCur int
1097	for _, res := range results {
1098		if res.Color != "" {
1099			// already has a color defined -- skip
1100			continue
1101		}
1102		if params.secondYAxis && res.SecondYAxis {
1103			res.LineWidth = params.rightWidth
1104			res.HasLineWidth = true
1105			if params.rightDashed && res.Dashed == 0 {
1106				res.Dashed = 2.5
1107			}
1108			res.Color = params.rightColor
1109		} else if params.secondYAxis {
1110			res.LineWidth = params.leftWidth
1111			res.HasLineWidth = true
1112			if params.leftDashed && res.Dashed == 0 {
1113				res.Dashed = 2.5
1114			}
1115			res.Color = params.leftColor
1116		}
1117		if res.Color == "" {
1118			res.Color = params.colorList[colorsCur]
1119			colorsCur++
1120			if colorsCur >= len(params.colorList) {
1121				colorsCur = 0
1122			}
1123		}
1124	}
1125
1126	if params.title != "" || params.vtitle != "" || params.vtitleRight != "" {
1127		titleSize := params.fontSize + math.Floor(math.Log(params.fontSize))
1128
1129		setColor(cr, params.fgColor)
1130		setFont(cr, params, titleSize)
1131	}
1132
1133	if params.title != "" {
1134		drawTitle(cr, params)
1135	}
1136	if params.vtitle != "" {
1137		drawVTitle(cr, params, params.vtitle, false)
1138	}
1139	if params.secondYAxis && params.vtitleRight != "" {
1140		drawVTitle(cr, params, params.vtitleRight, true)
1141	}
1142
1143	setFont(cr, params, params.fontSize)
1144	if !params.hideLegend {
1145		drawLegend(cr, params, results)
1146	}
1147
1148	// Setup axes, labels and grid
1149	// First we adjust the drawing area size to fit X-axis labels
1150	if !params.hideAxes {
1151		params.area.ymax -= params.fontExtents.Ascent * 2
1152	}
1153
1154	if !(params.lineMode == LineModeStaircase || ((minNumberOfPoints == maxNumberOfPoints) && (minNumberOfPoints == 2))) {
1155		params.endTime = 0
1156		for _, res := range results {
1157			tmp := int64(res.StopTime - res.StepTime)
1158			if params.endTime < tmp {
1159				params.endTime = tmp
1160			}
1161		}
1162		params.timeRange = params.endTime - params.startTime
1163		if params.timeRange < 0 {
1164			panic("startTime > endTime!!!")
1165		}
1166	}
1167
1168	// look for at least one stacked value
1169	for _, r := range results {
1170		if r.Stacked {
1171			params.hasStack = true
1172			break
1173		}
1174	}
1175
1176	// check if we need to stack all the things
1177	if params.areaMode == AreaModeStacked {
1178		params.hasStack = true
1179		for _, r := range results {
1180			r.Stacked = true
1181			r.StackName = "stack"
1182		}
1183	} else if params.areaMode == AreaModeFirst {
1184		results[0].Stacked = true
1185	} else if params.areaMode == AreaModeAll {
1186		for _, r := range results {
1187			r.Stacked = true
1188		}
1189	}
1190
1191	if params.hasStack {
1192		sort.Stable(ByStacked(results))
1193		// perform all aggregations / summations up so the rest of the graph drawing code doesn't need to care
1194
1195		var stackName = results[0].StackName
1196		var total []float64
1197		for _, r := range results {
1198			if r.DrawAsInfinite {
1199				continue
1200			}
1201
1202			// reached the end of the stacks -- we're done
1203			if !r.Stacked {
1204				break
1205			}
1206
1207			if r.StackName != stackName {
1208				// got to a new named stack -- reset accumulator
1209				total = total[:0]
1210				stackName = r.StackName
1211			}
1212
1213			vals := r.AggregatedValues()
1214			for i, v := range vals {
1215				if len(total) <= i {
1216					total = append(total, 0)
1217				}
1218
1219				if !math.IsNaN(v) {
1220					vals[i] += total[i]
1221					total[i] += v
1222				}
1223			}
1224
1225			// replace the values for the metric with our newly calculated ones
1226			// since these are now post-aggregation, reset the valuesPerPoint
1227			r.ValuesPerPoint = 1
1228			r.Values = vals
1229		}
1230	}
1231
1232	consolidateDataPoints(params, results)
1233
1234	currentXMin := params.area.xmin
1235	currentXMax := params.area.xmax
1236	if params.secondYAxis {
1237		setupTwoYAxes(cr, params, results)
1238	} else {
1239		setupYAxis(cr, params, results)
1240	}
1241
1242	for currentXMin != params.area.xmin || currentXMax != params.area.xmax {
1243		consolidateDataPoints(params, results)
1244		currentXMin = params.area.xmin
1245		currentXMax = params.area.xmax
1246		if params.secondYAxis {
1247			setupTwoYAxes(cr, params, results)
1248		} else {
1249			setupYAxis(cr, params, results)
1250		}
1251	}
1252
1253	setupXAxis(cr, params, results)
1254
1255	if !params.hideAxes {
1256		setColor(cr, params.fgColor)
1257		drawLabels(cr, params, results)
1258		if !params.hideGrid {
1259			drawGridLines(cr, params, results)
1260		}
1261	}
1262
1263	drawLines(cr, params, results)
1264}
1265
1266func consolidateDataPoints(params *Params, results []*types.MetricData) {
1267	numberOfPixels := params.area.xmax - params.area.xmin - (params.lineWidth + 1)
1268	params.graphWidth = numberOfPixels
1269
1270	for _, series := range results {
1271		numberOfDataPoints := math.Floor(float64(params.timeRange / int64(series.StepTime)))
1272		// minXStep := params.minXStep
1273		minXStep := 1.0
1274		divisor := float64(params.timeRange) / float64(series.StepTime)
1275		bestXStep := numberOfPixels / divisor
1276		if bestXStep < minXStep {
1277			drawableDataPoints := int(numberOfPixels / minXStep)
1278			pointsPerPixel := math.Ceil(numberOfDataPoints / float64(drawableDataPoints))
1279			// dumb variable naming :(
1280			series.SetValuesPerPoint(int(pointsPerPixel))
1281			series.XStep = (numberOfPixels * pointsPerPixel) / numberOfDataPoints
1282		} else {
1283			series.SetValuesPerPoint(1)
1284			series.XStep = bestXStep
1285		}
1286	}
1287}
1288
1289func setupTwoYAxes(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1290
1291	var Ldata []*types.MetricData
1292	var Rdata []*types.MetricData
1293
1294	var seriesWithMissingValuesL []*types.MetricData
1295	var seriesWithMissingValuesR []*types.MetricData
1296
1297	Ldata = params.dataLeft
1298	Rdata = params.dataRight
1299
1300	for _, s := range Ldata {
1301		for _, v := range s.Values {
1302			if math.IsNaN(v) {
1303				seriesWithMissingValuesL = append(seriesWithMissingValuesL, s)
1304				break
1305			}
1306		}
1307	}
1308
1309	for _, s := range Rdata {
1310		for _, v := range s.Values {
1311			if math.IsNaN(v) {
1312				seriesWithMissingValuesR = append(seriesWithMissingValuesR, s)
1313				break
1314			}
1315		}
1316
1317	}
1318
1319	yMinValueL := math.Inf(1)
1320	if params.drawNullAsZero && len(seriesWithMissingValuesL) > 0 {
1321		yMinValueL = 0
1322	} else {
1323		for _, s := range Ldata {
1324			if s.DrawAsInfinite {
1325				continue
1326			}
1327			for _, v := range s.AggregatedValues() {
1328				if math.IsNaN(v) {
1329					continue
1330				}
1331				if v < yMinValueL {
1332					yMinValueL = v
1333				}
1334			}
1335		}
1336	}
1337
1338	yMinValueR := math.Inf(1)
1339	if params.drawNullAsZero && len(seriesWithMissingValuesR) > 0 {
1340		yMinValueR = 0
1341	} else {
1342		for _, s := range Rdata {
1343			if s.DrawAsInfinite {
1344				continue
1345			}
1346			for _, v := range s.AggregatedValues() {
1347				if math.IsNaN(v) {
1348					continue
1349				}
1350				if v < yMinValueR {
1351					yMinValueR = v
1352				}
1353			}
1354		}
1355	}
1356
1357	var yMaxValueL, yMaxValueR float64
1358	yMaxValueL = math.Inf(-1)
1359	for _, s := range Ldata {
1360		for _, v := range s.AggregatedValues() {
1361			if math.IsNaN(v) {
1362				continue
1363			}
1364
1365			if v > yMaxValueL {
1366				yMaxValueL = v
1367			}
1368		}
1369	}
1370
1371	yMaxValueR = math.Inf(-1)
1372	for _, s := range Rdata {
1373		for _, v := range s.AggregatedValues() {
1374			if math.IsNaN(v) {
1375				continue
1376			}
1377
1378			if v > yMaxValueR {
1379				yMaxValueR = v
1380			}
1381		}
1382	}
1383
1384	if math.IsInf(yMinValueL, 1) {
1385		yMinValueL = 0
1386	}
1387
1388	if math.IsInf(yMinValueR, 1) {
1389		yMinValueR = 0
1390	}
1391
1392	if math.IsInf(yMaxValueL, -1) {
1393		yMaxValueL = 0
1394	}
1395	if math.IsInf(yMaxValueR, -1) {
1396		yMaxValueR = 0
1397	}
1398
1399	if !math.IsNaN(params.yMaxLeft) {
1400		yMaxValueL = params.yMaxLeft
1401	}
1402	if !math.IsNaN(params.yMaxRight) {
1403		yMaxValueR = params.yMaxRight
1404	}
1405
1406	if !math.IsNaN(params.yLimitLeft) && params.yLimitLeft < yMaxValueL {
1407		yMaxValueL = params.yLimitLeft
1408	}
1409	if !math.IsNaN(params.yLimitRight) && params.yLimitRight < yMaxValueR {
1410		yMaxValueR = params.yLimitRight
1411	}
1412
1413	if !math.IsNaN(params.yMinLeft) {
1414		yMinValueL = params.yMinLeft
1415	}
1416	if !math.IsNaN(params.yMinRight) {
1417		yMinValueR = params.yMinRight
1418	}
1419
1420	if yMaxValueL <= yMinValueL {
1421		yMaxValueL = yMinValueL + 1
1422	}
1423	if yMaxValueR <= yMinValueR {
1424		yMaxValueR = yMinValueR + 1
1425	}
1426
1427	yVarianceL := yMaxValueL - yMinValueL
1428	yVarianceR := yMaxValueR - yMinValueR
1429
1430	var orderL float64
1431	var orderFactorL float64
1432	if params.yUnitSystem == unitSystemBinary {
1433		orderL = math.Log2(yVarianceL)
1434		orderFactorL = math.Pow(2, math.Floor(orderL))
1435	} else {
1436		orderL = math.Log10(yVarianceL)
1437		orderFactorL = math.Pow(10, math.Floor(orderL))
1438	}
1439
1440	var orderR float64
1441	var orderFactorR float64
1442	if params.yUnitSystem == unitSystemBinary {
1443		orderR = math.Log2(yVarianceR)
1444		orderFactorR = math.Pow(2, math.Floor(orderR))
1445	} else {
1446		orderR = math.Log10(yVarianceR)
1447		orderFactorR = math.Pow(10, math.Floor(orderR))
1448	}
1449
1450	vL := yVarianceL / orderFactorL // we work with a scaled down yVariance for simplicity
1451	vR := yVarianceR / orderFactorR
1452
1453	yDivisors := params.yDivisors
1454
1455	prettyValues := []float64{0.1, 0.2, 0.25, 0.5, 1.0, 1.2, 1.25, 1.5, 2.0, 2.25, 2.5}
1456
1457	var divinfoL divisorInfo
1458	var divinfoR divisorInfo
1459
1460	for _, d := range yDivisors {
1461		qL := vL / d                                                              // our scaled down quotient, must be in the open interval (0,10)
1462		qR := vR / d                                                              // our scaled down quotient, must be in the open interval (0,10)
1463		pL := closest(qL, prettyValues)                                           // the prettyValue our quotient is closest to
1464		pR := closest(qR, prettyValues)                                           // the prettyValue our quotient is closest to
1465		divinfoL = append(divinfoL, yaxisDivisor{p: pL, diff: math.Abs(qL - pL)}) // make a  list so we can find the prettiest of the pretty
1466		divinfoR = append(divinfoR, yaxisDivisor{p: pR, diff: math.Abs(qR - pR)}) // make a  list so we can find the prettiest of the pretty
1467	}
1468
1469	sort.Sort(divinfoL)
1470	sort.Sort(divinfoR)
1471
1472	prettyValueL := divinfoL[0].p
1473	yStepL := prettyValueL * orderFactorL
1474
1475	prettyValueR := divinfoR[0].p
1476	yStepR := prettyValueR * orderFactorR
1477
1478	if !math.IsNaN(params.yStepL) {
1479		yStepL = params.yStepL
1480	}
1481	if !math.IsNaN(params.yStepR) {
1482		yStepR = params.yStepR
1483	}
1484
1485	params.yStepL = yStepL
1486	params.yStepR = yStepR
1487
1488	params.yBottomL = params.yStepL * math.Floor(yMinValueL/params.yStepL)
1489	params.yTopL = params.yStepL * math.Ceil(yMaxValueL/params.yStepL)
1490
1491	params.yBottomR = params.yStepR * math.Floor(yMinValueR/params.yStepR)
1492	params.yTopR = params.yStepR * math.Ceil(yMaxValueR/params.yStepR)
1493
1494	if params.logBase != 0 {
1495		if yMinValueL > 0 && yMinValueR > 0 {
1496			params.yBottomL = math.Pow(params.logBase, math.Floor(math.Log(yMinValueL)/math.Log(params.logBase)))
1497			params.yTopL = math.Pow(params.logBase, math.Ceil(math.Log(yMaxValueL/math.Log(params.logBase))))
1498			params.yBottomR = math.Pow(params.logBase, math.Floor(math.Log(yMinValueR)/math.Log(params.logBase)))
1499			params.yTopR = math.Pow(params.logBase, math.Ceil(math.Log(yMaxValueR/math.Log(params.logBase))))
1500		} else {
1501			panic("logscale with minvalue <= 0")
1502		}
1503	}
1504
1505	if !math.IsNaN(params.yMaxLeft) {
1506		params.yTopL = params.yMaxLeft
1507	}
1508	if !math.IsNaN(params.yMaxRight) {
1509		params.yTopR = params.yMaxRight
1510	}
1511	if !math.IsNaN(params.yMinLeft) {
1512		params.yBottomL = params.yMinLeft
1513	}
1514	if !math.IsNaN(params.yMinRight) {
1515		params.yBottomR = params.yMinRight
1516	}
1517
1518	params.ySpanL = params.yTopL - params.yBottomL
1519	params.ySpanR = params.yTopR - params.yBottomR
1520
1521	if params.ySpanL == 0 {
1522		params.yTopL++
1523		params.ySpanL++
1524	}
1525	if params.ySpanR == 0 {
1526		params.yTopR++
1527		params.ySpanR++
1528	}
1529
1530	params.graphHeight = params.area.ymax - params.area.ymin
1531	params.yScaleFactorL = params.graphHeight / params.ySpanL
1532	params.yScaleFactorR = params.graphHeight / params.ySpanR
1533
1534	params.yLabelValuesL = getYLabelValues(params, params.yBottomL, params.yTopL, params.yStepL)
1535	params.yLabelValuesR = getYLabelValues(params, params.yBottomR, params.yTopR, params.yStepR)
1536
1537	params.yLabelsL = make([]string, len(params.yLabelValuesL))
1538	for i, v := range params.yLabelValuesL {
1539		params.yLabelsL[i] = makeLabel(v, params.yStepL, params.ySpanL, params.yUnitSystem)
1540	}
1541
1542	params.yLabelsR = make([]string, len(params.yLabelValuesR))
1543	for i, v := range params.yLabelValuesR {
1544		params.yLabelsR[i] = makeLabel(v, params.yStepR, params.ySpanR, params.yUnitSystem)
1545	}
1546
1547	params.yLabelWidthL = 0
1548	for _, label := range params.yLabelsL {
1549		t := getTextExtents(cr, label)
1550		if t.XAdvance > params.yLabelWidthL {
1551			params.yLabelWidthL = t.XAdvance
1552		}
1553	}
1554
1555	params.yLabelWidthR = 0
1556	for _, label := range params.yLabelsR {
1557		t := getTextExtents(cr, label)
1558		if t.XAdvance > params.yLabelWidthR {
1559			params.yLabelWidthR = t.XAdvance
1560		}
1561	}
1562
1563	xMin := float64(params.margin) + (params.yLabelWidthL * 1.02)
1564	if params.area.xmin < xMin {
1565		params.area.xmin = xMin
1566	}
1567
1568	xMax := params.width - (params.yLabelWidthR * 1.02)
1569	if params.area.xmax > xMax {
1570		params.area.xmax = xMax
1571	}
1572}
1573
1574type yaxisDivisor struct {
1575	p    float64
1576	diff float64
1577}
1578
1579type divisorInfo []yaxisDivisor
1580
1581func (d divisorInfo) Len() int               { return len(d) }
1582func (d divisorInfo) Less(i int, j int) bool { return d[i].diff < d[j].diff }
1583func (d divisorInfo) Swap(i int, j int)      { d[i], d[j] = d[j], d[i] }
1584
1585func makeLabel(yValue, yStep, ySpan float64, yUnitSystem string) string {
1586	yValue, prefix := formatUnits(yValue, yStep, yUnitSystem)
1587	ySpan, spanPrefix := formatUnits(ySpan, yStep, yUnitSystem)
1588
1589	if prefix != "" {
1590		prefix += " "
1591	}
1592
1593	switch {
1594	case yValue < 0.1:
1595		return fmt.Sprintf("%.9g %s", yValue, prefix)
1596	case yValue < 1.0:
1597		return fmt.Sprintf("%.2f %s", yValue, prefix)
1598	case ySpan > 10 || spanPrefix != prefix:
1599		if yValue-math.Floor(yValue) < floatEpsilon {
1600			return fmt.Sprintf("%.1f %s", yValue, prefix)
1601		}
1602		return fmt.Sprintf("%d %s", int(yValue), prefix)
1603	case ySpan > 3:
1604		return fmt.Sprintf("%.1f %s", yValue, prefix)
1605	case ySpan > 0.1:
1606		return fmt.Sprintf("%.2f %s", yValue, prefix)
1607	default:
1608		return fmt.Sprintf("%g %s", yValue, prefix)
1609	}
1610}
1611
1612func setupYAxis(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1613	var seriesWithMissingValues []*types.MetricData
1614
1615	var yMinValue, yMaxValue float64
1616
1617	yMinValue, yMaxValue = math.NaN(), math.NaN()
1618	for _, r := range results {
1619		if r.DrawAsInfinite {
1620			continue
1621		}
1622		pushed := false
1623		for _, v := range r.AggregatedValues() {
1624			if math.IsNaN(v) && !pushed {
1625				seriesWithMissingValues = append(seriesWithMissingValues, r)
1626				pushed = true
1627			} else {
1628				if math.IsNaN(v) {
1629					continue
1630				}
1631				if !math.IsInf(v, 0) && (math.IsNaN(yMinValue) || yMinValue > v) {
1632					yMinValue = v
1633				}
1634				if !math.IsInf(v, 0) && (math.IsNaN(yMaxValue) || yMaxValue < v) {
1635					yMaxValue = v
1636				}
1637			}
1638		}
1639	}
1640
1641	if yMinValue > 0 && params.drawNullAsZero && len(seriesWithMissingValues) > 0 {
1642		yMinValue = 0
1643	}
1644
1645	if yMaxValue < 0 && params.drawNullAsZero && len(seriesWithMissingValues) > 0 {
1646		yMaxValue = 0
1647	}
1648
1649	// FIXME: Do we really need this check? It should be impossible to meet this conditions
1650	if math.IsNaN(yMinValue) {
1651		yMinValue = 0
1652	}
1653	if math.IsNaN(yMaxValue) {
1654		yMaxValue = 1
1655	}
1656
1657	if !math.IsNaN(params.yMax) {
1658		yMaxValue = params.yMax
1659	}
1660	if !math.IsNaN(params.yMin) {
1661		yMinValue = params.yMin
1662	}
1663
1664	if yMaxValue <= yMinValue {
1665		yMaxValue = yMinValue + 1
1666	}
1667
1668	yVariance := yMaxValue - yMinValue
1669
1670	var order float64
1671	var orderFactor float64
1672	if params.yUnitSystem == unitSystemBinary {
1673		order = math.Log2(yVariance)
1674		orderFactor = math.Pow(2, math.Floor(order))
1675	} else {
1676		order = math.Log10(yVariance)
1677		orderFactor = math.Pow(10, math.Floor(order))
1678	}
1679
1680	v := yVariance / orderFactor // we work with a scaled down yVariance for simplicity
1681
1682	yDivisors := params.yDivisors
1683
1684	prettyValues := []float64{0.1, 0.2, 0.25, 0.5, 1.0, 1.2, 1.25, 1.5, 2.0, 2.25, 2.5}
1685
1686	var divinfo divisorInfo
1687
1688	for _, d := range yDivisors {
1689		q := v / d                                                           // our scaled down quotient, must be in the open interval (0,10)
1690		p := closest(q, prettyValues)                                        // the prettyValue our quotient is closest to
1691		divinfo = append(divinfo, yaxisDivisor{p: p, diff: math.Abs(q - p)}) // make a  list so we can find the prettiest of the pretty
1692	}
1693
1694	sort.Sort(divinfo) // sort our pretty values by 'closeness to a factor"
1695
1696	prettyValue := divinfo[0].p        // our winner! Y-axis will have labels placed at multiples of our prettyValue
1697	yStep := prettyValue * orderFactor // scale it back up to the order of yVariance
1698
1699	if !math.IsNaN(params.yStep) {
1700		yStep = params.yStep
1701	}
1702
1703	params.yStep = yStep
1704
1705	params.yBottom = params.yStep * math.Floor(yMinValue/params.yStep+floatEpsilon) // start labels at the greatest multiple of yStep <= yMinValue
1706	params.yTop = params.yStep * math.Ceil(yMaxValue/params.yStep-floatEpsilon)     // Extend the top of our graph to the lowest yStep multiple >= yMaxValue
1707
1708	if params.logBase != 0 {
1709		if yMinValue > 0 {
1710			params.yBottom = math.Pow(params.logBase, math.Floor(math.Log(yMinValue)/math.Log(params.logBase)))
1711			params.yTop = math.Pow(params.logBase, math.Ceil(math.Log(yMaxValue)/math.Log(params.logBase)))
1712		} else {
1713			panic("logscale with minvalue <= 0")
1714			// raise GraphError('Logarithmic scale specified with a dataset with a minimum value less than or equal to zero')
1715		}
1716	}
1717
1718	/*
1719	   if 'yMax' in self.params:
1720	     if self.params['yMax'] == 'max':
1721	       scale = 1.0 * yMaxValue / self.yTop
1722	       self.yStep *= (scale - 0.000001)
1723	       self.yTop = yMaxValue
1724	     else:
1725	       self.yTop = self.params['yMax'] * 1.0
1726	   if 'yMin' in self.params:
1727	     self.yBottom = self.params['yMin']
1728	*/
1729
1730	params.ySpan = params.yTop - params.yBottom
1731
1732	if params.ySpan == 0 {
1733		params.yTop++
1734		params.ySpan++
1735	}
1736
1737	params.graphHeight = params.area.ymax - params.area.ymin
1738	params.yScaleFactor = params.graphHeight / params.ySpan
1739
1740	if !params.hideAxes {
1741		// Create and measure the Y-labels
1742
1743		params.yLabelValues = getYLabelValues(params, params.yBottom, params.yTop, params.yStep)
1744
1745		params.yLabels = make([]string, len(params.yLabelValues))
1746		for i, v := range params.yLabelValues {
1747			params.yLabels[i] = makeLabel(v, params.yStep, params.ySpan, params.yUnitSystem)
1748		}
1749
1750		params.yLabelWidth = 0
1751		for _, label := range params.yLabels {
1752			t := getTextExtents(cr, label)
1753			if t.XAdvance > params.yLabelWidth {
1754				params.yLabelWidth = t.XAdvance
1755			}
1756		}
1757
1758		if !params.hideYAxis {
1759			if params.yAxisSide == YAxisSideLeft { // scoot the graph over to the left just enough to fit the y-labels
1760				xMin := float64(params.margin) + float64(params.yLabelWidth)*1.02
1761				if params.area.xmin < xMin {
1762					params.area.xmin = xMin
1763				}
1764			} else { // scoot the graph over to the right just enough to fit the y-labels
1765				// xMin := 0 // TODO(dgryski): bug?  Why is this set?
1766				xMax := float64(params.margin) - float64(params.yLabelWidth)*1.02
1767				if params.area.xmax >= xMax {
1768					params.area.xmax = xMax
1769				}
1770			}
1771		}
1772	} else {
1773		params.yLabelValues = nil
1774		params.yLabels = nil
1775		params.yLabelWidth = 0.0
1776	}
1777}
1778
1779func getFontExtents(cr *cairoSurfaceContext) cairo.FontExtents {
1780	// TODO(dgryski): allow font options
1781	/*
1782	   if fontOptions:
1783	     self.setFont(**fontOptions)
1784	*/
1785	var F cairo.FontExtents
1786	cr.context.FontExtents(&F)
1787	return F
1788}
1789
1790func getTextExtents(cr *cairoSurfaceContext, text string) cairo.TextExtents {
1791	// TODO(dgryski): allow font options
1792	/*
1793	   if fontOptions:
1794	     self.setFont(**fontOptions)
1795	*/
1796	var T cairo.TextExtents
1797	cr.context.TextExtents(text, &T)
1798	return T
1799}
1800
1801// formatUnits formats the given value according to the given unit prefix system
1802func formatUnits(v, step float64, system string) (float64, string) {
1803
1804	var condition func(float64) bool
1805
1806	if step == math.NaN() {
1807		condition = func(size float64) bool { return math.Abs(v) >= size }
1808	} else {
1809		condition = func(size float64) bool { return math.Abs(v) >= size && step >= size }
1810	}
1811
1812	unitsystem := unitSystems[system]
1813
1814	for _, p := range unitsystem {
1815		fsize := float64(p.size)
1816		if condition(fsize) {
1817			v2 := v / fsize
1818			if (v2-math.Floor(v2)) < floatEpsilon && v > 1 {
1819				v2 = math.Floor(v2)
1820			}
1821			return v2, p.prefix
1822		}
1823	}
1824
1825	if (v-math.Floor(v)) < floatEpsilon && v > 1 {
1826		v = math.Floor(v)
1827	}
1828	return v, ""
1829}
1830
1831func getYLabelValues(params *Params, minYValue, maxYValue, yStep float64) []float64 {
1832	if params.logBase != 0 {
1833		return logrange(params.logBase, minYValue, maxYValue)
1834	}
1835
1836	return frange(minYValue, maxYValue, yStep)
1837}
1838
1839func logrange(base, scaleMin, scaleMax float64) []float64 {
1840	current := scaleMin
1841	if scaleMin > 0 {
1842		current = math.Floor(math.Log(scaleMin) / math.Log(base))
1843	}
1844	factor := current
1845	var vals []float64
1846	for current < scaleMax {
1847		current = math.Pow(base, factor)
1848		vals = append(vals, current)
1849		factor++
1850	}
1851	return vals
1852}
1853
1854func frange(start, end, step float64) []float64 {
1855	var vals []float64
1856	f := start
1857	for f <= (end + floatEpsilon) {
1858		vals = append(vals, f)
1859		f += step
1860		// Protect against rounding errors on very small float ranges
1861		if f == start {
1862			vals = append(vals, end)
1863			break
1864		}
1865	}
1866	return vals
1867}
1868
1869func closest(number float64, neighbours []float64) float64 {
1870	distance := math.Inf(1)
1871	var closestNeighbor float64
1872	for _, n := range neighbours {
1873		d := math.Abs(n - number)
1874		if d < distance {
1875			distance = d
1876			closestNeighbor = n
1877		}
1878	}
1879
1880	return closestNeighbor
1881}
1882
1883func setupXAxis(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1884
1885	/*
1886	   if self.userTimeZone:
1887	     tzinfo = pytz.timezone(self.userTimeZone)
1888	   else:
1889	     tzinfo = pytz.timezone(settings.TIME_ZONE)
1890	*/
1891
1892	/*
1893
1894		self.start_dt = datetime.fromtimestamp(self.startTime, tzinfo)
1895		self.end_dt = datetime.fromtimestamp(self.endTime, tzinfo)
1896	*/
1897
1898	secondsPerPixel := float64(params.timeRange) / float64(params.graphWidth)
1899	params.xScaleFactor = float64(params.graphWidth) / float64(params.timeRange)
1900
1901	for _, c := range xAxisConfigs {
1902		if c.seconds <= secondsPerPixel && c.maxInterval >= params.timeRange {
1903			params.xConf = c
1904		}
1905	}
1906
1907	if params.xConf.seconds == 0 {
1908		params.xConf = xAxisConfigs[len(xAxisConfigs)-1]
1909	}
1910
1911	params.xLabelStep = int64(params.xConf.labelUnit) * params.xConf.labelStep
1912	params.xMinorGridStep = int64(float64(params.xConf.minorGridUnit) * params.xConf.minorGridStep)
1913	params.xMajorGridStep = int64(params.xConf.majorGridUnit) * params.xConf.majorGridStep
1914}
1915
1916func drawLabels(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1917	if !params.hideYAxis {
1918		drawYAxis(cr, params, results)
1919	}
1920	if !params.hideXAxis {
1921		drawXAxis(cr, params, results)
1922	}
1923}
1924
1925func drawYAxis(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
1926	var x float64
1927	if params.secondYAxis {
1928
1929		for _, value := range params.yLabelValuesL {
1930			label := makeLabel(value, params.yStepL, params.ySpanL, params.yUnitSystem)
1931			y := getYCoord(params, value, YCoordSideLeft)
1932			if y < 0 {
1933				y = 0
1934			}
1935
1936			x = params.area.xmin - float64(params.yLabelWidthL)*0.02
1937			drawText(cr, params, label, x, y, HAlignRight, VAlignCenter, 0)
1938
1939		}
1940
1941		for _, value := range params.yLabelValuesR {
1942			label := makeLabel(value, params.yStepR, params.ySpanR, params.yUnitSystem)
1943			y := getYCoord(params, value, YCoordSideRight)
1944			if y < 0 {
1945				y = 0
1946			}
1947
1948			x = params.area.xmax + float64(params.yLabelWidthR)*0.02 + 3
1949			drawText(cr, params, label, x, y, HAlignLeft, VAlignCenter, 0)
1950		}
1951		return
1952	}
1953
1954	for _, value := range params.yLabelValues {
1955		label := makeLabel(value, params.yStep, params.ySpan, params.yUnitSystem)
1956		y := getYCoord(params, value, YCoordSideNone)
1957		if y < 0 {
1958			y = 0
1959		}
1960
1961		if params.yAxisSide == YAxisSideLeft {
1962			x = params.area.xmin - float64(params.yLabelWidth)*0.02
1963			drawText(cr, params, label, x, y, HAlignRight, VAlignCenter, 0)
1964		} else {
1965			x = params.area.xmax + float64(params.yLabelWidth)*0.02
1966			drawText(cr, params, label, x, y, HAlignLeft, VAlignCenter, 0)
1967		}
1968	}
1969}
1970
1971func findXTimes(start int64, unit TimeUnit, step float64) (int64, int64) {
1972
1973	t := time.Unix(int64(start), 0)
1974
1975	var d time.Duration
1976
1977	switch unit {
1978	case Second:
1979		d = time.Second
1980	case Minute:
1981		d = time.Minute
1982	case Hour:
1983		d = time.Hour
1984	case Day:
1985		d = 24 * time.Hour
1986	default:
1987		panic("invalid unit")
1988	}
1989
1990	d *= time.Duration(step)
1991	t = t.Truncate(d)
1992
1993	for t.Unix() < int64(start) {
1994		t = t.Add(d)
1995	}
1996
1997	return t.Unix(), int64(d / time.Second)
1998}
1999
2000func drawXAxis(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
2001
2002	dt, xDelta := findXTimes(int64(params.startTime), params.xConf.labelUnit, float64(params.xConf.labelStep))
2003
2004	xFormat := params.xFormat
2005	if xFormat == "" {
2006		xFormat = params.xConf.format
2007	}
2008
2009	maxAscent := getFontExtents(cr).Ascent
2010
2011	for dt < int64(params.endTime) {
2012		label, _ := strftime.Format(xFormat, time.Unix(int64(dt), 0).In(params.tz))
2013		x := params.area.xmin + float64(dt-params.startTime)*params.xScaleFactor
2014		y := params.area.ymax + maxAscent
2015		drawText(cr, params, label, x, y, HAlignCenter, VAlignTop, 0)
2016		dt += xDelta
2017	}
2018}
2019
2020func drawGridLines(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
2021	// Horizontal grid lines
2022	leftside := params.area.xmin
2023	rightside := params.area.xmax
2024	top := params.area.ymin
2025	bottom := params.area.ymax
2026
2027	var labels []float64
2028	if params.secondYAxis {
2029		labels = params.yLabelValuesL
2030	} else {
2031		labels = params.yLabelValues
2032	}
2033
2034	for i, value := range labels {
2035		cr.context.SetLineWidth(0.4)
2036		setColor(cr, string2RGBA(params.majorGridLineColor))
2037
2038		var y float64
2039		if params.secondYAxis {
2040			y = getYCoord(params, value, YCoordSideLeft)
2041		} else {
2042			y = getYCoord(params, value, YCoordSideNone)
2043		}
2044
2045		if math.IsNaN(y) || y < 0 {
2046			continue
2047		}
2048
2049		cr.context.MoveTo(leftside, y)
2050		cr.context.LineTo(rightside, y)
2051		cr.context.Stroke()
2052
2053		// draw minor gridlines if this isn't the last label
2054		if params.minorY >= 1 && i < len(labels)-1 {
2055			valueLower, valueUpper := value, labels[i+1]
2056
2057			// each minor gridline is 1/minorY apart from the nearby gridlines.
2058			// we calculate that distance, for adding to the value in the loop.
2059			distance := ((valueUpper - valueLower) / float64(1+params.minorY))
2060
2061			// starting from the initial valueLower, we add the minor distance
2062			// for each minor gridline that we wish to draw, and then draw it.
2063			for minor := 0; minor < params.minorY; minor++ {
2064				cr.context.SetLineWidth(0.3)
2065				setColor(cr, string2RGBA(params.minorGridLineColor))
2066
2067				// the current minor gridline value is halfway between the current and next major gridline values
2068				value = (valueLower + ((1 + float64(minor)) * distance))
2069
2070				var yTopFactor float64
2071				if params.logBase != 0 {
2072					yTopFactor = params.logBase * params.logBase
2073				} else {
2074					yTopFactor = 1
2075				}
2076
2077				if params.secondYAxis {
2078					if value >= (yTopFactor * params.yTopL) {
2079						continue
2080					}
2081				} else {
2082					if value >= (yTopFactor * params.yTop) {
2083						continue
2084					}
2085
2086				}
2087
2088				if params.secondYAxis {
2089					y = getYCoord(params, value, YCoordSideLeft)
2090				} else {
2091					y = getYCoord(params, value, YCoordSideNone)
2092				}
2093
2094				if math.IsNaN(y) || y < 0 {
2095					continue
2096				}
2097
2098				cr.context.MoveTo(leftside, y)
2099				cr.context.LineTo(rightside, y)
2100				cr.context.Stroke()
2101			}
2102
2103		}
2104
2105	}
2106
2107	// Vertical grid lines
2108
2109	// First we do the minor grid lines (majors will paint over them)
2110	cr.context.SetLineWidth(0.25)
2111	setColor(cr, string2RGBA(params.minorGridLineColor))
2112	dt, xMinorDelta := findXTimes(params.startTime, params.xConf.minorGridUnit, params.xConf.minorGridStep)
2113
2114	for dt < params.endTime {
2115		x := params.area.xmin + float64(dt-params.startTime)*params.xScaleFactor
2116
2117		if x < params.area.xmax {
2118			cr.context.MoveTo(x, bottom)
2119			cr.context.LineTo(x, top)
2120			cr.context.Stroke()
2121		}
2122
2123		dt += xMinorDelta
2124	}
2125
2126	// Now we do the major grid lines
2127	cr.context.SetLineWidth(0.33)
2128	setColor(cr, string2RGBA(params.majorGridLineColor))
2129	dt, xMajorDelta := findXTimes(params.startTime, params.xConf.majorGridUnit, float64(params.xConf.majorGridStep))
2130
2131	for dt < params.endTime {
2132		x := params.area.xmin + float64(dt-params.startTime)*params.xScaleFactor
2133
2134		if x < params.area.xmax {
2135			cr.context.MoveTo(x, bottom)
2136			cr.context.LineTo(x, top)
2137			cr.context.Stroke()
2138		}
2139
2140		dt += xMajorDelta
2141	}
2142
2143	// Draw side borders for our graph area
2144	cr.context.SetLineWidth(0.5)
2145	cr.context.MoveTo(params.area.xmax, bottom)
2146	cr.context.LineTo(params.area.xmax, top)
2147	cr.context.MoveTo(params.area.xmin, bottom)
2148	cr.context.LineTo(params.area.xmin, top)
2149	cr.context.Stroke()
2150}
2151
2152func str2linecap(s string) cairo.LineCap {
2153	switch s {
2154	case "butt":
2155		return cairo.LineCapButt
2156	case "round":
2157		return cairo.LineCapRound
2158	case "square":
2159		return cairo.LineCapSquare
2160	}
2161	return cairo.LineCapButt
2162}
2163
2164func str2linejoin(s string) cairo.LineJoin {
2165	switch s {
2166	case "miter":
2167		return cairo.LineJoinMiter
2168	case "round":
2169		return cairo.LineJoinRound
2170	case "bevel":
2171		return cairo.LineJoinBevel
2172	}
2173	return cairo.LineJoinMiter
2174}
2175
2176func getYCoord(params *Params, value float64, side YCoordSide) (y float64) {
2177
2178	var yLabelValues []float64
2179	var yTop float64
2180	var yBottom float64
2181
2182	switch side {
2183	case YCoordSideLeft:
2184		yLabelValues = params.yLabelValuesL
2185		yTop = params.yTopL
2186		yBottom = params.yBottomL
2187	case YCoordSideRight:
2188		yLabelValues = params.yLabelValuesR
2189		yTop = params.yTopR
2190		yBottom = params.yBottomR
2191	default:
2192		yLabelValues = params.yLabelValues
2193		yTop = params.yTop
2194		yBottom = params.yBottom
2195	}
2196
2197	var highestValue float64
2198	var lowestValue float64
2199
2200	if yLabelValues != nil {
2201		highestValue = yLabelValues[len(yLabelValues)-1]
2202		lowestValue = yLabelValues[0]
2203	} else {
2204		highestValue = yTop
2205		lowestValue = yBottom
2206	}
2207	pixelRange := params.area.ymax - params.area.ymin
2208	relativeValue := (value - lowestValue)
2209	valueRange := (highestValue - lowestValue)
2210	if params.logBase != 0 {
2211		if value <= 0 {
2212			return math.NaN()
2213		}
2214		relativeValue = (math.Log(value) / math.Log(params.logBase)) - (math.Log(lowestValue) / math.Log(params.logBase))
2215		valueRange = (math.Log(highestValue) / math.Log(params.logBase)) - (math.Log(lowestValue) / math.Log(params.logBase))
2216	}
2217	pixelToValueRatio := (pixelRange / valueRange)
2218	valueInPixels := (pixelToValueRatio * relativeValue)
2219	return params.area.ymax - valueInPixels
2220}
2221
2222func drawLines(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
2223
2224	linecap := "butt"
2225	linejoin := "miter"
2226
2227	cr.context.SetLineWidth(params.lineWidth)
2228
2229	originalWidth := params.lineWidth
2230
2231	cr.context.SetDash(nil, 0)
2232
2233	cr.context.SetLineCap(str2linecap(linecap))
2234	cr.context.SetLineJoin(str2linejoin(linejoin))
2235
2236	if !math.IsNaN(params.areaAlpha) {
2237		alpha := params.areaAlpha
2238		var strokeSeries []*types.MetricData
2239		for _, r := range results {
2240			if r.Stacked {
2241				r.Alpha = alpha
2242				r.HasAlpha = true
2243
2244				newSeries := types.MetricData{
2245					FetchResponse: pb.FetchResponse{
2246						Name:              r.Name,
2247						StopTime:          r.StopTime,
2248						StartTime:         r.StartTime,
2249						StepTime:          r.AggregatedTimeStep(),
2250						Values:            make([]float64, len(r.AggregatedValues())),
2251						XFilesFactor:      0,
2252						PathExpression:    r.Name,
2253						ConsolidationFunc: "average",
2254					},
2255					ValuesPerPoint: 1,
2256					GraphOptions: types.GraphOptions{
2257						Color:       r.Color,
2258						XStep:       r.XStep,
2259						SecondYAxis: r.SecondYAxis,
2260					},
2261				}
2262				copy(newSeries.Values, r.AggregatedValues())
2263				strokeSeries = append(strokeSeries, &newSeries)
2264			}
2265		}
2266		if len(strokeSeries) > 0 {
2267			results = append(results, strokeSeries...)
2268		}
2269	}
2270
2271	cr.context.SetLineWidth(1.0)
2272	cr.context.Rectangle(params.area.xmin, params.area.ymin, (params.area.xmax - params.area.xmin), (params.area.ymax - params.area.ymin))
2273	cr.context.Clip()
2274	cr.context.SetLineWidth(originalWidth)
2275
2276	cr.context.Save()
2277	clipRestored := false
2278	for _, series := range results {
2279
2280		if !series.Stacked && !clipRestored {
2281			cr.context.Restore()
2282			clipRestored = true
2283		}
2284
2285		if series.HasLineWidth {
2286			cr.context.SetLineWidth(series.LineWidth)
2287		} else {
2288			cr.context.SetLineWidth(params.lineWidth)
2289		}
2290
2291		if series.Dashed != 0 {
2292			cr.context.SetDash([]float64{series.Dashed}, 1)
2293		}
2294
2295		if series.Invisible {
2296			setColorAlpha(cr, color.RGBA{0, 0, 0, 0}, 0)
2297		} else if series.HasAlpha {
2298			setColorAlpha(cr, string2RGBA(series.Color), series.Alpha)
2299		} else {
2300			setColor(cr, string2RGBA(series.Color))
2301		}
2302
2303		missingPoints := float64(int64(series.StartTime)-params.startTime) / float64(series.StepTime)
2304		startShift := series.XStep * (missingPoints / float64(series.ValuesPerPoint))
2305		x := float64(params.area.xmin) + startShift + (params.lineWidth / 2.0)
2306		y := float64(params.area.ymin)
2307		origX := x
2308		startX := x
2309
2310		consecutiveNones := 0
2311		for index, value := range series.AggregatedValues() {
2312			x = origX + (float64(index) * series.XStep)
2313
2314			if params.drawNullAsZero && math.IsNaN(value) {
2315				value = 0
2316			}
2317
2318			if math.IsNaN(value) {
2319				if consecutiveNones == 0 {
2320					cr.context.LineTo(x, y)
2321					if series.Stacked {
2322						if params.secondYAxis {
2323							if series.SecondYAxis {
2324								fillAreaAndClip(cr, params, x, y, startX, getYCoord(params, 0, YCoordSideRight))
2325							} else {
2326								fillAreaAndClip(cr, params, x, y, startX, getYCoord(params, 0, YCoordSideLeft))
2327							}
2328						} else {
2329							fillAreaAndClip(cr, params, x, y, startX, getYCoord(params, 0, YCoordSideNone))
2330						}
2331					}
2332				}
2333				consecutiveNones++
2334			} else {
2335				if params.secondYAxis {
2336					if series.SecondYAxis {
2337						y = getYCoord(params, value, YCoordSideRight)
2338					} else {
2339						y = getYCoord(params, value, YCoordSideLeft)
2340					}
2341				} else {
2342					y = getYCoord(params, value, YCoordSideNone)
2343				}
2344				if math.IsNaN(y) {
2345					value = y
2346				} else {
2347					if y < 0 {
2348						y = 0
2349					}
2350				}
2351				if series.DrawAsInfinite && value > 0 {
2352					cr.context.MoveTo(x, params.area.ymax)
2353					cr.context.LineTo(x, params.area.ymin)
2354					cr.context.Stroke()
2355					continue
2356				}
2357				if consecutiveNones > 0 {
2358					startX = x
2359				}
2360
2361				if !math.IsNaN(y) {
2362					switch params.lineMode {
2363
2364					case LineModeStaircase:
2365						if consecutiveNones > 0 {
2366							cr.context.MoveTo(x, y)
2367						} else {
2368							cr.context.LineTo(x, y)
2369						}
2370					case LineModeSlope:
2371						if consecutiveNones > 0 {
2372							cr.context.MoveTo(x, y)
2373						}
2374					case LineModeConnected:
2375						if consecutiveNones > params.connectedLimit || consecutiveNones == index {
2376							cr.context.MoveTo(x, y)
2377						}
2378					}
2379
2380					cr.context.LineTo(x, y)
2381				}
2382				consecutiveNones = 0
2383			}
2384		}
2385
2386		if series.Stacked {
2387			var areaYFrom float64
2388			if params.secondYAxis {
2389				if series.SecondYAxis {
2390					areaYFrom = getYCoord(params, 0, YCoordSideRight)
2391				} else {
2392					areaYFrom = getYCoord(params, 0, YCoordSideLeft)
2393				}
2394			} else {
2395				areaYFrom = getYCoord(params, 0, YCoordSideNone)
2396			}
2397			fillAreaAndClip(cr, params, x, y, startX, areaYFrom)
2398		} else {
2399			cr.context.Stroke()
2400		}
2401		cr.context.SetLineWidth(originalWidth)
2402
2403		if series.Dashed != 0 {
2404			cr.context.SetDash(nil, 0)
2405		}
2406	}
2407}
2408
2409type SeriesLegend struct {
2410	name        string
2411	color       string
2412	secondYAxis bool
2413}
2414
2415func drawLegend(cr *cairoSurfaceContext, params *Params, results []*types.MetricData) {
2416	const (
2417		padding = 5
2418	)
2419	var longestName string
2420	var longestNameLen int
2421	var uniqueNames map[string]bool
2422	var numRight int
2423	var legend []SeriesLegend
2424	if params.uniqueLegend {
2425		uniqueNames = make(map[string]bool)
2426	}
2427
2428	for _, res := range results {
2429		nameLen := len(res.Name)
2430		if nameLen == 0 {
2431			continue
2432		}
2433		if nameLen > longestNameLen {
2434			longestNameLen = nameLen
2435			longestName = res.Name
2436		}
2437		if res.SecondYAxis {
2438			numRight++
2439		}
2440		if params.uniqueLegend {
2441			if _, ok := uniqueNames[res.Name]; !ok {
2442				var tmp = SeriesLegend{
2443					res.Name,
2444					res.Color,
2445					res.SecondYAxis,
2446				}
2447				uniqueNames[res.Name] = true
2448				legend = append(legend, tmp)
2449			}
2450		} else {
2451			var tmp = SeriesLegend{
2452				res.Name,
2453				res.Color,
2454				res.SecondYAxis,
2455			}
2456			legend = append(legend, tmp)
2457		}
2458	}
2459
2460	rightSideLabels := false
2461	testSizeName := longestName + " " + longestName
2462	var textExtents cairo.TextExtents
2463	cr.context.TextExtents(testSizeName, &textExtents)
2464	testWidth := textExtents.XAdvance + 2*(params.fontExtents.Height+padding)
2465	if testWidth+50 < params.width {
2466		rightSideLabels = true
2467	}
2468
2469	cr.context.TextExtents(longestName, &textExtents)
2470	boxSize := params.fontExtents.Height - 1
2471	lineHeight := params.fontExtents.Height + 1
2472	labelWidth := textExtents.XAdvance + 2*(boxSize+padding)
2473	cr.context.SetLineWidth(1.0)
2474	x := params.area.xmin
2475
2476	if params.secondYAxis && rightSideLabels {
2477		columns := math.Max(1, math.Floor(math.Floor((params.width-params.area.xmin)/labelWidth)/2.0))
2478		numberOfLines := math.Max(float64(len(results)-numRight), float64(numRight))
2479		legendHeight := math.Max(1, (numberOfLines/columns)) * (lineHeight + padding)
2480		params.area.ymax -= legendHeight
2481		y := params.area.ymax + (2 * padding)
2482
2483		xRight := params.area.xmax - params.area.xmin
2484		yRight := y
2485		nRight := 0
2486		n := 0
2487		for _, item := range legend {
2488			setColor(cr, string2RGBA(item.color))
2489			if item.secondYAxis {
2490				nRight++
2491				drawRectangle(cr, params, xRight-padding, yRight, boxSize, boxSize, true)
2492				color := colors["darkgray"]
2493				setColor(cr, color)
2494				drawRectangle(cr, params, xRight-padding, yRight, boxSize, boxSize, false)
2495				setColor(cr, params.fgColor)
2496				drawText(cr, params, item.name, xRight-boxSize, yRight, HAlignRight, VAlignTop, 0.0)
2497				xRight -= labelWidth
2498				if nRight%int(columns) == 0 {
2499					xRight = params.area.xmax - params.area.xmin
2500					yRight += lineHeight
2501				}
2502			} else {
2503				n++
2504				drawRectangle(cr, params, x, y, boxSize, boxSize, true)
2505				color := colors["darkgray"]
2506				setColor(cr, color)
2507				drawRectangle(cr, params, x, y, boxSize, boxSize, false)
2508				setColor(cr, params.fgColor)
2509				drawText(cr, params, item.name, x+boxSize+padding, y, HAlignLeft, VAlignTop, 0.0)
2510				x += labelWidth
2511				if n%int(columns) == 0 {
2512					x = params.area.xmin
2513					y += lineHeight
2514				}
2515			}
2516		}
2517		return
2518	}
2519	// else
2520	columns := math.Max(1, math.Floor(params.width/labelWidth))
2521	numberOfLines := math.Ceil(float64(len(results)) / columns)
2522	legendHeight := (numberOfLines * lineHeight) + padding
2523	params.area.ymax -= legendHeight
2524	y := params.area.ymax + (2 * padding)
2525	cnt := 0
2526	for _, item := range legend {
2527		setColor(cr, string2RGBA(item.color))
2528		if item.secondYAxis {
2529			drawRectangle(cr, params, x+labelWidth+padding, y, boxSize, boxSize, true)
2530			color := colors["darkgray"]
2531			setColor(cr, color)
2532			drawRectangle(cr, params, x+labelWidth+padding, y, boxSize, boxSize, false)
2533			setColor(cr, params.fgColor)
2534			drawText(cr, params, item.name, x+labelWidth, y, HAlignRight, VAlignTop, 0.0)
2535			x += labelWidth
2536		} else {
2537			drawRectangle(cr, params, x, y, boxSize, boxSize, true)
2538			color := colors["darkgray"]
2539			setColor(cr, color)
2540			drawRectangle(cr, params, x, y, boxSize, boxSize, false)
2541			setColor(cr, params.fgColor)
2542			drawText(cr, params, item.name, x+boxSize+padding, y, HAlignLeft, VAlignTop, 0.0)
2543			x += labelWidth
2544		}
2545		if (cnt+1)%int(columns) == 0 {
2546			x = params.area.xmin
2547			y += lineHeight
2548		}
2549		cnt++
2550	}
2551	return
2552}
2553
2554func drawTitle(cr *cairoSurfaceContext, params *Params) {
2555	y := params.area.ymin
2556	x := params.width / 2.0
2557	lines := strings.Split(params.title, "\n")
2558	lineHeight := params.fontExtents.Height
2559
2560	for _, line := range lines {
2561		drawText(cr, params, line, x, y, HAlignCenter, VAlignTop, 0.0)
2562		y += lineHeight
2563	}
2564	params.area.ymin = y
2565	if params.yAxisSide != YAxisSideRight {
2566		params.area.ymin += float64(params.margin)
2567	}
2568}
2569
2570func drawVTitle(cr *cairoSurfaceContext, params *Params, title string, rightAlign bool) {
2571	lineHeight := params.fontExtents.Height
2572
2573	if rightAlign {
2574		x := params.area.xmax - lineHeight
2575		y := params.height / 2.0
2576		for _, line := range strings.Split(title, "\n") {
2577			drawText(cr, params, line, x, y, HAlignCenter, VAlignBaseline, 90.0)
2578			x -= lineHeight
2579		}
2580		params.area.xmax = x - float64(params.margin) - lineHeight
2581	} else {
2582		x := params.area.xmin + lineHeight
2583		y := params.height / 2.0
2584		for _, line := range strings.Split(title, "\n") {
2585			drawText(cr, params, line, x, y, HAlignCenter, VAlignBaseline, 270.0)
2586			x += lineHeight
2587		}
2588		params.area.xmin = x + float64(params.margin) + lineHeight
2589	}
2590}
2591
2592func radians(angle float64) float64 {
2593	const x = math.Pi / 180
2594	return angle * x
2595}
2596
2597func drawText(cr *cairoSurfaceContext, params *Params, text string, x, y float64, align HAlign, valign VAlign, rotate float64) {
2598	var hAlign, vAlign float64
2599	var textExtents cairo.TextExtents
2600	var fontExtents cairo.FontExtents
2601	var origMatrix cairo.Matrix
2602	cr.context.TextExtents(text, &textExtents)
2603	cr.context.FontExtents(&fontExtents)
2604
2605	cr.context.GetMatrix(&origMatrix)
2606	angle := radians(rotate)
2607	angleSin, angleCos := math.Sincos(angle)
2608
2609	switch align {
2610	case HAlignLeft:
2611		hAlign = 0.0
2612	case HAlignCenter:
2613		hAlign = textExtents.XAdvance / 2.0
2614	case HAlignRight:
2615		hAlign = textExtents.XAdvance
2616	}
2617	switch valign {
2618	case VAlignTop:
2619		vAlign = fontExtents.Ascent
2620	case VAlignCenter:
2621		vAlign = fontExtents.Height/2.0 - fontExtents.Descent
2622	case VAlignBottom:
2623		vAlign = -fontExtents.Descent
2624	case VAlignBaseline:
2625		vAlign = 0.0
2626	}
2627
2628	cr.context.MoveTo(x, y)
2629	cr.context.RelMoveTo(angleSin*(-vAlign), angleCos*vAlign)
2630	cr.context.Rotate(angle)
2631	cr.context.RelMoveTo(-hAlign, 0)
2632	cr.context.TextPath(text)
2633	cr.context.Fill()
2634	cr.context.SetMatrix(&origMatrix)
2635}
2636
2637func setColorAlpha(cr *cairoSurfaceContext, color color.RGBA, alpha float64) {
2638	r, g, b, _ := color.RGBA()
2639	cr.context.SetSourceRGBA(float64(r)/65536, float64(g)/65536, float64(b)/65536, alpha)
2640}
2641
2642func setColor(cr *cairoSurfaceContext, color color.RGBA) {
2643	r, g, b, a := color.RGBA()
2644	cr.context.SetSourceRGBA(float64(r)/65536, float64(g)/65536, float64(b)/65536, float64(a)/65536)
2645}
2646
2647func setFont(cr *cairoSurfaceContext, params *Params, size float64) {
2648	cr.context.SelectFontFace(params.fontName, params.fontItalic, params.fontBold)
2649	cr.context.SetFontSize(size)
2650	cr.context.FontExtents(&params.fontExtents)
2651}
2652
2653func drawRectangle(cr *cairoSurfaceContext, params *Params, x float64, y float64, w float64, h float64, fill bool) {
2654	if !fill {
2655		offset := cr.context.GetLineWidth() / 2.0
2656		x += offset
2657		y += offset
2658		h -= offset
2659		w -= offset
2660	}
2661	cr.context.Rectangle(x, y, w, h)
2662	if fill {
2663		cr.context.Fill()
2664	} else {
2665		cr.context.SetDash(nil, 0)
2666		cr.context.Stroke()
2667	}
2668}
2669
2670func fillAreaAndClip(cr *cairoSurfaceContext, params *Params, x, y, startX, areaYFrom float64) {
2671
2672	if math.IsNaN(startX) {
2673		startX = params.area.xmin
2674	}
2675
2676	if math.IsNaN(areaYFrom) {
2677		areaYFrom = params.area.ymax
2678	}
2679
2680	pattern := cr.context.CopyPath()
2681
2682	// fill
2683	cr.context.LineTo(x, areaYFrom)      // bottom endX
2684	cr.context.LineTo(startX, areaYFrom) // bottom startX
2685	cr.context.ClosePath()
2686	if params.areaMode == AreaModeAll {
2687		cr.context.FillPreserve()
2688	} else {
2689		cr.context.Fill()
2690	}
2691
2692	// clip above y axis
2693	cr.context.AppendPath(pattern)
2694	cr.context.LineTo(x, areaYFrom)                       // yZero endX
2695	cr.context.LineTo(params.area.xmax, areaYFrom)        // yZero right
2696	cr.context.LineTo(params.area.xmax, params.area.ymin) // top right
2697	cr.context.LineTo(params.area.xmin, params.area.ymin) // top left
2698	cr.context.LineTo(params.area.xmin, areaYFrom)        // yZero left
2699	cr.context.LineTo(startX, areaYFrom)                  // yZero startX
2700
2701	// clip below y axis
2702	cr.context.LineTo(x, areaYFrom)                       // yZero endX
2703	cr.context.LineTo(params.area.xmax, areaYFrom)        // yZero right
2704	cr.context.LineTo(params.area.xmax, params.area.ymax) // bottom right
2705	cr.context.LineTo(params.area.xmin, params.area.ymax) // bottom left
2706	cr.context.LineTo(params.area.xmin, areaYFrom)        // yZero left
2707	cr.context.LineTo(startX, areaYFrom)                  // yZero startX
2708	cr.context.ClosePath()
2709	cr.context.Clip()
2710}
2711
2712type ByStacked []*types.MetricData
2713
2714func (b ByStacked) Len() int { return len(b) }
2715
2716func (b ByStacked) Less(i int, j int) bool {
2717	return (b[i].Stacked && !b[j].Stacked) || (b[i].Stacked && b[j].Stacked && b[i].StackName < b[j].StackName)
2718}
2719
2720func (b ByStacked) Swap(i int, j int) { b[i], b[j] = b[j], b[i] }
2721