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