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, ¶ms, 0, 0, params.width, params.height, true) 1007 1008 drawGraph(cr, ¶ms, 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(¶ms.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] }