github.com/go-graphite/carbonapi@v0.17.0/expr/functions/cairo/png/cairo.go (about)

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