From c98cf2bf8188bc70bf31c42af1fdff8da766c005 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Wed, 10 Apr 2024 23:25:40 -0700 Subject: [PATCH 01/12] line charts wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/lc_go | 165 +++++++++++++++++++ tools/rw-heatmaps/pkg/chart/line_chart.go | 187 ++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 tools/rw-heatmaps/lc_go create mode 100644 tools/rw-heatmaps/pkg/chart/line_chart.go diff --git a/tools/rw-heatmaps/lc_go b/tools/rw-heatmaps/lc_go new file mode 100644 index 00000000000..ff6743573a0 --- /dev/null +++ b/tools/rw-heatmaps/lc_go @@ -0,0 +1,165 @@ +// Copyright 2024 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package chart + +import ( + "fmt" + "sort" + + "gonum.org/v1/plot" + "gonum.org/v1/plot/font" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/plotutil" + "gonum.org/v1/plot/vg" + "gonum.org/v1/plot/vg/draw" + "gonum.org/v1/plot/vg/vgimg" + + "go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset" +) + +/* +type lineChart struct { +} +*/ + +// PlotLineCharts creates a new line chart. +func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error { + plot.DefaultFont = font.Font{ + Typeface: "Liberation", + Variant: "Sans", + } + + ratios := datasets[0].GetSortedRatios() + for _, ratio := range ratios { + records := make([][]dataset.DataRecord, len(datasets)) + for i, d := range datasets { + records[i] = d.Records[ratio] + } + canvas := plotLineChart(ratio, records, title) + if err := saveCanvas(canvas, fmt.Sprintf("%0.04f", ratio), outputImageFile, outputFormat); err != nil { + return err + } + } + + return nil +} + +func plotLineChart(ratio float64, records [][]dataset.DataRecord, title string) *vgimg.Canvas { + // Make a 4x2 grid of heatmaps. + const rows, cols = 4, 2 + + // Set the width and height of the canvas. + const width, height = 30 * vg.Centimeter, 40 * vg.Centimeter + + canvas := vgimg.New(width, height) + dc := draw.New(canvas) + + // Create a tiled layout for the plots. + t := draw.Tiles{ + Rows: rows, + Cols: cols, + PadX: vg.Millimeter * 4, + PadY: vg.Millimeter * 4, + PadTop: vg.Millimeter * 10, + PadBottom: vg.Millimeter * 2, + PadLeft: vg.Millimeter * 2, + PadRight: vg.Millimeter * 2, + } + + plots := make([][]*plot.Plot, rows) + for i := range plots { + plots[i] = make([]*plot.Plot, cols) + } + + var values []int + rec := make(map[int64][]dataset.DataRecord) + for _, r := range records[0] { + if _, ok := rec[r.ValueSize]; !ok { + values = append(values, int(r.ValueSize)) + } + rec[r.ValueSize] = append(rec[r.ValueSize], r) + } + sort.Ints(values) + + row, col := 0, 0 + for _, value := range values { + records := rec[int64(value)] + plots[row][col] = plotIndividualLineChart(fmt.Sprintf("Value Size: %d", value), records) + + if col++; col == cols { + col = 0 + row++ + } + } + + // Fill the canvas with the plots and legends. + canvases := plot.Align(plots, t, dc) + for i := 0; i < rows; i++ { + for j := 0; j < cols; j++ { + // Continue if there is no plot in the current cell (incomplete data). + if plots[i][j] == nil { + continue + } + + c := draw.Crop(canvases[i][j], 0, 0, 0, 0) + plots[i][j].Draw(c) + } + } + + // Add the title and parameter legend. + l := plot.NewLegend() + l.Add(fmt.Sprintf("%s R/W Ratio %0.04f", title, ratio)) + // TODO: Add the parameter legend. + //l.Add(param) + l.Top = true + l.Left = true + l.Draw(dc) + + return canvas +} + +func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.Plot { + p := plot.New() + p.Title.Text = title + p.X.Label.Text = "Connections Amount" + p.X.Scale = plot.LogScale{} + p.X.Tick.Marker = pow2Ticks{} + p.Y.Label.Text = "QPS (Requests/sec)" + p.Y.Scale = plot.LogScale{} + p.Y.Tick.Marker = pow2Ticks{} + + readPts := make(plotter.XYs, len(records)) + writePts := make(plotter.XYs, len(records)) + qr := make(map[int64]struct{}) + for i, record := range records { + writePts[i].X = float64(record.ConnSize) + readPts[i].X = writePts[i].X + readPts[i].Y = record.AvgRead + writePts[i].Y = record.AvgWrite + qr[record.ValueSize] = struct{}{} + } + + fmt.Println("qr:") + for k := range qr { + fmt.Println(k) + } + fmt.Println("qr end") + + if err := plotutil.AddLinePoints(p, "read", readPts, "write", writePts); err != nil { + panic(err) + } + + return p +} diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go new file mode 100644 index 00000000000..4914f41df4e --- /dev/null +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -0,0 +1,187 @@ +// Copyright 2024 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package chart + +import ( + "fmt" + "image/color" + "sort" + + "gonum.org/v1/plot" + "gonum.org/v1/plot/font" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" + "gonum.org/v1/plot/vg/draw" + "gonum.org/v1/plot/vg/vgimg" + + "go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset" +) + +/* +type lineChart struct { +} +*/ + +// PlotLineCharts creates a new line chart. +func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error { + plot.DefaultFont = font.Font{ + Typeface: "Liberation", + Variant: "Sans", + } + + var canvas *vgimg.Canvas + canvas = plotLineChart(datasets, title) + if err := saveCanvas(canvas, "readwrite", outputImageFile, outputFormat); err != nil { + return err + } + + return nil +} + +func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { + // Make a 4x2 grid of heatmaps. + const rows, cols = 4, 2 + + // Set the width and height of the canvas. + const width, height = 30 * vg.Centimeter, 40 * vg.Centimeter + + canvas := vgimg.New(width, height) + dc := draw.New(canvas) + + // Create a tiled layout for the plots. + t := draw.Tiles{ + Rows: rows, + Cols: cols, + PadX: vg.Millimeter * 4, + PadY: vg.Millimeter * 4, + PadTop: vg.Millimeter * 10, + PadBottom: vg.Millimeter * 2, + PadLeft: vg.Millimeter * 2, + PadRight: vg.Millimeter * 2, + } + + plots := make([][]*plot.Plot, rows) + for i := range plots { + plots[i] = make([]*plot.Plot, cols) + } + + // Load records into the grid. + ratios := datasets[0].GetSortedRatios() + row, col := 0, 0 + for _, ratio := range ratios { + records := datasets[0].Records[ratio] + plots[row][col] = plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records) + + if col++; col == cols { + col = 0 + row++ + } + } + + // Fill the canvas with the plots and legends. + canvases := plot.Align(plots, t, dc) + for i := 0; i < rows; i++ { + for j := 0; j < cols; j++ { + // Continue if there is no plot in the current cell (incomplete data). + if plots[i][j] == nil { + continue + } + + c := draw.Crop(canvases[i][j], 0, 0, 0, 0) + plots[i][j].Draw(c) + } + } + + // Add the title and parameter legend. + l := plot.NewLegend() + l.Add(title) + l.Add(datasets[0].Param) + l.Top = true + l.Left = true + l.Draw(dc) + + return canvas +} + +func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.Plot { + p := plot.New() + p.Title.Text = title + p.X.Label.Text = "Connections Amount" + p.X.Scale = plot.LogScale{} + p.X.Tick.Marker = pow2Ticks{} + p.Y.Label.Text = "QPS (Requests/sec)" + p.Y.Scale = plot.LogScale{} + p.Y.Tick.Marker = pow2Ticks{} + + var values []int + rec := make(map[int64][]dataset.DataRecord) + for _, r := range records { + if _, ok := rec[r.ValueSize]; !ok { + values = append(values, int(r.ValueSize)) + } + rec[r.ValueSize] = append(rec[r.ValueSize], r) + } + sort.Ints(values) + + var DefaultGlyphShapes = []draw.GlyphDrawer{ + draw.RingGlyph{}, + draw.SquareGlyph{}, + draw.TriangleGlyph{}, + draw.CrossGlyph{}, + draw.PlusGlyph{}, + draw.CircleGlyph{}, + draw.BoxGlyph{}, + draw.PyramidGlyph{}, + } + + for i, value := range values { + r := rec[int64(value)] + readPts := make(plotter.XYs, len(r)) + writePts := make(plotter.XYs, len(r)) + for i, record := range r { + writePts[i].X = float64(record.ConnSize) + readPts[i].X = writePts[i].X + readPts[i].Y = record.AvgRead + writePts[i].Y = record.AvgWrite + } + + l, s, err := plotter.NewLinePoints(readPts) + if err != nil { + panic(err) + } + l.Color = color.RGBA{241, 90, 96, 255} + s.Color = l.Color + s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] + p.Add(l, s) + if i == 0 { + p.Legend.Add("read", plot.Thumbnailer(l)) + } + + l, s, err = plotter.NewLinePoints(writePts) + if err != nil { + panic(err) + } + l.Color = color.RGBA{90, 155, 212, 255} + l.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + s.Color = l.Color + s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] + p.Add(l, s) + if i == 0 { + p.Legend.Add("write", plot.Thumbnailer(l)) + } + } + + return p +} From b43cd0523d696dbce7085bd027b5e9cae1a3bb56 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Thu, 2 May 2024 22:48:58 -0700 Subject: [PATCH 02/12] wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/cmd/root.go | 3 +- tools/rw-heatmaps/pkg/chart/line_chart.go | 37 +++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/tools/rw-heatmaps/cmd/root.go b/tools/rw-heatmaps/cmd/root.go index 49b98a9fca2..ff305b2243e 100644 --- a/tools/rw-heatmaps/cmd/root.go +++ b/tools/rw-heatmaps/cmd/root.go @@ -56,7 +56,8 @@ func NewRootCommand() *cobra.Command { } } - return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered) + return chart.PlotLineCharts(datasets, o.title, o.outputImageFile, o.outputFormat) + //return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered) }, } diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 4914f41df4e..c2e87fa7545 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -52,10 +52,10 @@ func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputF func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { // Make a 4x2 grid of heatmaps. - const rows, cols = 4, 2 + const rows, cols = 8, 1 // Set the width and height of the canvas. - const width, height = 30 * vg.Centimeter, 40 * vg.Centimeter + const width, height = 30 * vg.Centimeter, 100 * vg.Centimeter canvas := vgimg.New(width, height) dc := draw.New(canvas) @@ -73,8 +73,10 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { } plots := make([][]*plot.Plot, rows) + legends := make([][]plot.Legend, rows) for i := range plots { plots[i] = make([]*plot.Plot, cols) + legends[i] = make([]plot.Legend, cols) } // Load records into the grid. @@ -82,7 +84,9 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { row, col := 0, 0 for _, ratio := range ratios { records := datasets[0].Records[ratio] - plots[row][col] = plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records) + p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records) + plots[row][col] = p + legends[row][col] = l if col++; col == cols { col = 0 @@ -99,7 +103,14 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { continue } - c := draw.Crop(canvases[i][j], 0, 0, 0, 0) + l := legends[i][j] + r := l.Rectangle(canvases[i][j]) + legendWidth := r.Max.X - r.Min.X + // Adjust the legend down a little. + l.YOffs = plots[i][j].Title.TextStyle.FontExtents().Height * 3 + l.Draw(canvases[i][j]) + + c := draw.Crop(canvases[i][j], 0, -legendWidth-vg.Millimeter, 0, 0) plots[i][j].Draw(c) } } @@ -115,7 +126,7 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { return canvas } -func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.Plot { +func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot.Plot, plot.Legend) { p := plot.New() p.Title.Text = title p.X.Label.Text = "Connections Amount" @@ -125,6 +136,8 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.P p.Y.Scale = plot.LogScale{} p.Y.Tick.Marker = pow2Ticks{} + legend := plot.NewLegend() + var values []int rec := make(map[int64][]dataset.DataRecord) for _, r := range records { @@ -166,7 +179,7 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.P s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] p.Add(l, s) if i == 0 { - p.Legend.Add("read", plot.Thumbnailer(l)) + legend.Add("read", plot.Thumbnailer(l)) } l, s, err = plotter.NewLinePoints(writePts) @@ -174,14 +187,20 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.P panic(err) } l.Color = color.RGBA{90, 155, 212, 255} - l.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + //l.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} s.Color = l.Color s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] p.Add(l, s) if i == 0 { - p.Legend.Add("write", plot.Thumbnailer(l)) + legend.Add("write", plot.Thumbnailer(l)) } + + sc, _ := plotter.NewScatter(writePts) + sc.Color = color.RGBA{0, 0, 0, 255} + sc.Shape = s.Shape + sc.XYs = s.XYs + legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(sc)) } - return p + return p, legend } From 727898cd338dafc1c084d9038c073341648de4c5 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Sat, 4 May 2024 21:03:50 -0700 Subject: [PATCH 03/12] more wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index c2e87fa7545..721e9e1ce34 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -22,6 +22,7 @@ import ( "gonum.org/v1/plot" "gonum.org/v1/plot/font" "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" "gonum.org/v1/plot/vg/vgimg" @@ -148,17 +149,6 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. } sort.Ints(values) - var DefaultGlyphShapes = []draw.GlyphDrawer{ - draw.RingGlyph{}, - draw.SquareGlyph{}, - draw.TriangleGlyph{}, - draw.CrossGlyph{}, - draw.PlusGlyph{}, - draw.CircleGlyph{}, - draw.BoxGlyph{}, - draw.PyramidGlyph{}, - } - for i, value := range values { r := rec[int64(value)] readPts := make(plotter.XYs, len(r)) @@ -174,9 +164,9 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. if err != nil { panic(err) } - l.Color = color.RGBA{241, 90, 96, 255} + l.Color = plotutil.Color(0) s.Color = l.Color - s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] + s.Shape = plotutil.Shape(i) p.Add(l, s) if i == 0 { legend.Add("read", plot.Thumbnailer(l)) @@ -186,10 +176,10 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. if err != nil { panic(err) } - l.Color = color.RGBA{90, 155, 212, 255} + l.Color = plotutil.Color(1) //l.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} s.Color = l.Color - s.Shape = DefaultGlyphShapes[i%len(DefaultGlyphShapes)] + s.Shape = plotutil.Shape(i) p.Add(l, s) if i == 0 { legend.Add("write", plot.Thumbnailer(l)) From ac9237a8ad465577ed04dcbaed6b95b298cc0857 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Wed, 4 Dec 2024 23:12:34 -0800 Subject: [PATCH 04/12] wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 52 +++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 721e9e1ce34..776c8e9c074 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -84,8 +84,11 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { ratios := datasets[0].GetSortedRatios() row, col := 0, 0 for _, ratio := range ratios { - records := datasets[0].Records[ratio] - p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records) + var records [][]dataset.DataRecord + for _, d := range datasets { + records = append(records, d.Records[ratio]) + } + p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records...) plots[row][col] = p legends[row][col] = l @@ -127,7 +130,7 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { return canvas } -func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot.Plot, plot.Legend) { +func plotIndividualLineChart(title string, records ...[]dataset.DataRecord) (*plot.Plot, plot.Legend) { p := plot.New() p.Title.Text = title p.X.Label.Text = "Connections Amount" @@ -139,16 +142,41 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. legend := plot.NewLegend() - var values []int - rec := make(map[int64][]dataset.DataRecord) - for _, r := range records { - if _, ok := rec[r.ValueSize]; !ok { - values = append(values, int(r.ValueSize)) + values := getSortedValueSizes(records...) + for i, rs := range records { + rec := make(map[int64][]dataset.DataRecord) + for _, r := range rs { + rec[r.ValueSize] = append(rec[r.ValueSize], r) + } + if len(records) > 0 { + // TODO: Add the filename to the legend. + addValues(p, legend, values, rec, i) + } else { + addValues(p, legend, values, rec, i) } - rec[r.ValueSize] = append(rec[r.ValueSize], r) + } + + return p, legend +} + +func getSortedValueSizes(records ...[]dataset.DataRecord) []int { + valueMap := make(map[int64]struct{}) + for _, rs := range records { + for _, r := range rs { + valueMap[r.ValueSize] = struct{}{} + } + } + + var values []int + for v := range valueMap { + values = append(values, int(v)) } sort.Ints(values) + return values +} + +func addValues(p *plot.Plot, legend plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int) { for i, value := range values { r := rec[int64(value)] readPts := make(plotter.XYs, len(r)) @@ -164,7 +192,7 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. if err != nil { panic(err) } - l.Color = plotutil.Color(0) + l.Color = plotutil.Color(index * 2) s.Color = l.Color s.Shape = plotutil.Shape(i) p.Add(l, s) @@ -176,8 +204,7 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. if err != nil { panic(err) } - l.Color = plotutil.Color(1) - //l.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + l.Color = plotutil.Color(index*2 + 1) s.Color = l.Color s.Shape = plotutil.Shape(i) p.Add(l, s) @@ -192,5 +219,4 @@ func plotIndividualLineChart(title string, records []dataset.DataRecord) (*plot. legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(sc)) } - return p, legend } From 6233658eddb93a981076c3d40391c309135ab251 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Thu, 5 Dec 2024 21:33:37 -0800 Subject: [PATCH 05/12] adjust to number of ratios Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 776c8e9c074..dde41c88a96 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -15,8 +15,10 @@ package chart import ( + "cmp" "fmt" "image/color" + "slices" "sort" "gonum.org/v1/plot" @@ -52,11 +54,18 @@ func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputF } func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { - // Make a 4x2 grid of heatmaps. - const rows, cols = 8, 1 + maxRatios := func() int { + max := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int { + return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios())) + }) + return len(max.GetSortedRatios()) + }() + + // Make a nx1 grid of heatmaps. + rows, cols := maxRatios, 1 // Set the width and height of the canvas. - const width, height = 30 * vg.Centimeter, 100 * vg.Centimeter + width, height := 30*vg.Centimeter, 15*font.Length(maxRatios)*vg.Centimeter canvas := vgimg.New(width, height) dc := draw.New(canvas) From 8fdeb559ea221ade15d5c18e34a47a42f0cfedbd Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Thu, 5 Dec 2024 21:58:00 -0800 Subject: [PATCH 06/12] wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index dde41c88a96..8caa581c960 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -76,7 +76,7 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { Cols: cols, PadX: vg.Millimeter * 4, PadY: vg.Millimeter * 4, - PadTop: vg.Millimeter * 10, + PadTop: vg.Millimeter * 15, PadBottom: vg.Millimeter * 2, PadLeft: vg.Millimeter * 2, PadRight: vg.Millimeter * 2, @@ -131,7 +131,9 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { // Add the title and parameter legend. l := plot.NewLegend() l.Add(title) - l.Add(datasets[0].Param) + for _, d := range datasets { + l.Add(fmt.Sprintf("%s: %s", d.FileName, d.Param)) + } l.Top = true l.Left = true l.Draw(dc) @@ -159,9 +161,9 @@ func plotIndividualLineChart(title string, records ...[]dataset.DataRecord) (*pl } if len(records) > 0 { // TODO: Add the filename to the legend. - addValues(p, legend, values, rec, i) + addValues(p, &legend, values, rec, i) } else { - addValues(p, legend, values, rec, i) + addValues(p, &legend, values, rec, i) } } @@ -185,7 +187,7 @@ func getSortedValueSizes(records ...[]dataset.DataRecord) []int { return values } -func addValues(p *plot.Plot, legend plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int) { +func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int) { for i, value := range values { r := rec[int64(value)] readPts := make(plotter.XYs, len(r)) From d27d4a70973fb381c942d6506ef513b1f089f207 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Fri, 6 Dec 2024 22:19:22 -0800 Subject: [PATCH 07/12] wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 70 +++++++++++------------ 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 8caa581c960..7dc6d73d420 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -32,11 +32,6 @@ import ( "go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset" ) -/* -type lineChart struct { -} -*/ - // PlotLineCharts creates a new line chart. func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error { plot.DefaultFont = font.Font{ @@ -54,7 +49,7 @@ func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputF } func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { - maxRatios := func() int { + ratiosLength := func() int { max := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int { return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios())) }) @@ -62,10 +57,10 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { }() // Make a nx1 grid of heatmaps. - rows, cols := maxRatios, 1 + rows, cols := ratiosLength, 1 // Set the width and height of the canvas. - width, height := 30*vg.Centimeter, 15*font.Length(maxRatios)*vg.Centimeter + width, height := 30*vg.Centimeter, 15*font.Length(ratiosLength)*vg.Centimeter canvas := vgimg.New(width, height) dc := draw.New(canvas) @@ -90,14 +85,19 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { } // Load records into the grid. - ratios := datasets[0].GetSortedRatios() + ratios := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int { + return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios())) + }).GetSortedRatios() + row, col := 0, 0 for _, ratio := range ratios { var records [][]dataset.DataRecord + var fileNames []string for _, d := range datasets { records = append(records, d.Records[ratio]) + fileNames = append(fileNames, d.FileName) } - p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records...) + p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records, fileNames) plots[row][col] = p legends[row][col] = l @@ -141,7 +141,7 @@ func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas { return canvas } -func plotIndividualLineChart(title string, records ...[]dataset.DataRecord) (*plot.Plot, plot.Legend) { +func plotIndividualLineChart(title string, records [][]dataset.DataRecord, fileNames []string) (*plot.Plot, plot.Legend) { p := plot.New() p.Title.Text = title p.X.Label.Text = "Connections Amount" @@ -159,11 +159,10 @@ func plotIndividualLineChart(title string, records ...[]dataset.DataRecord) (*pl for _, r := range rs { rec[r.ValueSize] = append(rec[r.ValueSize], r) } - if len(records) > 0 { - // TODO: Add the filename to the legend. - addValues(p, &legend, values, rec, i) + if len(records) > 1 { + addValues(p, &legend, values, rec, i, fileNames[i]) } else { - addValues(p, &legend, values, rec, i) + addValues(p, &legend, values, rec, i, "") } } @@ -187,7 +186,7 @@ func getSortedValueSizes(records ...[]dataset.DataRecord) []int { return values } -func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int) { +func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int, fileName string) { for i, value := range values { r := rec[int64(value)] readPts := make(plotter.XYs, len(r)) @@ -199,35 +198,34 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] writePts[i].Y = record.AvgWrite } - l, s, err := plotter.NewLinePoints(readPts) + readLine, s, err := plotter.NewLinePoints(readPts) if err != nil { panic(err) } - l.Color = plotutil.Color(index * 2) - s.Color = l.Color + readLine.Color = plotutil.Color(index * 2) + s.Color = readLine.Color s.Shape = plotutil.Shape(i) - p.Add(l, s) - if i == 0 { - legend.Add("read", plot.Thumbnailer(l)) - } + p.Add(readLine, s) - l, s, err = plotter.NewLinePoints(writePts) + writeLine, s, err := plotter.NewLinePoints(writePts) if err != nil { panic(err) } - l.Color = plotutil.Color(index*2 + 1) - s.Color = l.Color + writeLine.Color = plotutil.Color(index*2 + 1) + s.Color = writeLine.Color s.Shape = plotutil.Shape(i) - p.Add(l, s) - if i == 0 { - legend.Add("write", plot.Thumbnailer(l)) + p.Add(writeLine, s) + + if index == 0 { + sc, _ := plotter.NewScatter(writePts) + sc.Color = color.RGBA{0, 0, 0, 255} + sc.Shape = s.Shape + sc.XYs = s.XYs + legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(sc)) + } + if i == len(values)-1 { + legend.Add(fmt.Sprintf("read %s", fileName), plot.Thumbnailer(readLine)) + legend.Add(fmt.Sprintf("write %s", fileName), plot.Thumbnailer(writeLine)) } - - sc, _ := plotter.NewScatter(writePts) - sc.Color = color.RGBA{0, 0, 0, 255} - sc.Shape = s.Shape - sc.XYs = s.XYs - legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(sc)) } - } From 16dffa9439b2a58b03e29dafc5d074814c64328a Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Fri, 6 Dec 2024 22:25:09 -0800 Subject: [PATCH 08/12] wip Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/cmd/root.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tools/rw-heatmaps/cmd/root.go b/tools/rw-heatmaps/cmd/root.go index ff305b2243e..a0b6ab2f8cb 100644 --- a/tools/rw-heatmaps/cmd/root.go +++ b/tools/rw-heatmaps/cmd/root.go @@ -33,6 +33,8 @@ var ( ErrMissingInputFileArg = fmt.Errorf("missing input file argument") // ErrInvalidOutputFormat is returned when the output format is invalid. ErrInvalidOutputFormat = fmt.Errorf("invalid output format, must be one of png, jpg, jpeg, tiff") + // ErrInvalidChartTYpe is returned when the chart type is invalid. + ErrInvalidChartType = fmt.Errorf("invalid chart type, must be one of line, heatmap") ) // NewRootCommand returns the root command for the rw-heatmaps tool. @@ -56,8 +58,11 @@ func NewRootCommand() *cobra.Command { } } - return chart.PlotLineCharts(datasets, o.title, o.outputImageFile, o.outputFormat) - //return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered) + if o.chartType == "line" { + return chart.PlotLineCharts(datasets, o.title, o.outputImageFile, o.outputFormat) + } + + return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered) }, } @@ -71,6 +76,7 @@ type options struct { outputImageFile string outputFormat string zeroCentered bool + chartType string } // newOptions returns a new options for the command with the default values applied. @@ -78,6 +84,7 @@ func newOptions() options { return options{ outputFormat: "jpg", zeroCentered: true, + chartType: "heatmap", } } @@ -87,6 +94,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) { fs.StringVarP(&o.outputImageFile, "output-image-file", "o", o.outputImageFile, "output image filename (required)") fs.StringVarP(&o.outputFormat, "output-format", "f", o.outputFormat, "output image file format") fs.BoolVar(&o.zeroCentered, "zero-centered", o.zeroCentered, "plot the improvement graph with white color represents 0.0") + fs.StringVarP(&o.chartType, "chart-type", "c", o.chartType, "type of chart to plot (line, heatmap)") } // Validate returns an error if the options are invalid. @@ -102,5 +110,10 @@ func (o *options) Validate() error { default: return ErrInvalidOutputFormat } + switch o.chartType { + case "line", "heatmap": + default: + return ErrInvalidChartType + } return nil } From 95985d54dedcaed93858720ad1f374257596a638 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Fri, 6 Dec 2024 22:31:05 -0800 Subject: [PATCH 09/12] delete temp file Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/lc_go | 165 ---------------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 tools/rw-heatmaps/lc_go diff --git a/tools/rw-heatmaps/lc_go b/tools/rw-heatmaps/lc_go deleted file mode 100644 index ff6743573a0..00000000000 --- a/tools/rw-heatmaps/lc_go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2024 The etcd Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package chart - -import ( - "fmt" - "sort" - - "gonum.org/v1/plot" - "gonum.org/v1/plot/font" - "gonum.org/v1/plot/plotter" - "gonum.org/v1/plot/plotutil" - "gonum.org/v1/plot/vg" - "gonum.org/v1/plot/vg/draw" - "gonum.org/v1/plot/vg/vgimg" - - "go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset" -) - -/* -type lineChart struct { -} -*/ - -// PlotLineCharts creates a new line chart. -func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error { - plot.DefaultFont = font.Font{ - Typeface: "Liberation", - Variant: "Sans", - } - - ratios := datasets[0].GetSortedRatios() - for _, ratio := range ratios { - records := make([][]dataset.DataRecord, len(datasets)) - for i, d := range datasets { - records[i] = d.Records[ratio] - } - canvas := plotLineChart(ratio, records, title) - if err := saveCanvas(canvas, fmt.Sprintf("%0.04f", ratio), outputImageFile, outputFormat); err != nil { - return err - } - } - - return nil -} - -func plotLineChart(ratio float64, records [][]dataset.DataRecord, title string) *vgimg.Canvas { - // Make a 4x2 grid of heatmaps. - const rows, cols = 4, 2 - - // Set the width and height of the canvas. - const width, height = 30 * vg.Centimeter, 40 * vg.Centimeter - - canvas := vgimg.New(width, height) - dc := draw.New(canvas) - - // Create a tiled layout for the plots. - t := draw.Tiles{ - Rows: rows, - Cols: cols, - PadX: vg.Millimeter * 4, - PadY: vg.Millimeter * 4, - PadTop: vg.Millimeter * 10, - PadBottom: vg.Millimeter * 2, - PadLeft: vg.Millimeter * 2, - PadRight: vg.Millimeter * 2, - } - - plots := make([][]*plot.Plot, rows) - for i := range plots { - plots[i] = make([]*plot.Plot, cols) - } - - var values []int - rec := make(map[int64][]dataset.DataRecord) - for _, r := range records[0] { - if _, ok := rec[r.ValueSize]; !ok { - values = append(values, int(r.ValueSize)) - } - rec[r.ValueSize] = append(rec[r.ValueSize], r) - } - sort.Ints(values) - - row, col := 0, 0 - for _, value := range values { - records := rec[int64(value)] - plots[row][col] = plotIndividualLineChart(fmt.Sprintf("Value Size: %d", value), records) - - if col++; col == cols { - col = 0 - row++ - } - } - - // Fill the canvas with the plots and legends. - canvases := plot.Align(plots, t, dc) - for i := 0; i < rows; i++ { - for j := 0; j < cols; j++ { - // Continue if there is no plot in the current cell (incomplete data). - if plots[i][j] == nil { - continue - } - - c := draw.Crop(canvases[i][j], 0, 0, 0, 0) - plots[i][j].Draw(c) - } - } - - // Add the title and parameter legend. - l := plot.NewLegend() - l.Add(fmt.Sprintf("%s R/W Ratio %0.04f", title, ratio)) - // TODO: Add the parameter legend. - //l.Add(param) - l.Top = true - l.Left = true - l.Draw(dc) - - return canvas -} - -func plotIndividualLineChart(title string, records []dataset.DataRecord) *plot.Plot { - p := plot.New() - p.Title.Text = title - p.X.Label.Text = "Connections Amount" - p.X.Scale = plot.LogScale{} - p.X.Tick.Marker = pow2Ticks{} - p.Y.Label.Text = "QPS (Requests/sec)" - p.Y.Scale = plot.LogScale{} - p.Y.Tick.Marker = pow2Ticks{} - - readPts := make(plotter.XYs, len(records)) - writePts := make(plotter.XYs, len(records)) - qr := make(map[int64]struct{}) - for i, record := range records { - writePts[i].X = float64(record.ConnSize) - readPts[i].X = writePts[i].X - readPts[i].Y = record.AvgRead - writePts[i].Y = record.AvgWrite - qr[record.ValueSize] = struct{}{} - } - - fmt.Println("qr:") - for k := range qr { - fmt.Println(k) - } - fmt.Println("qr end") - - if err := plotutil.AddLinePoints(p, "read", readPts, "write", writePts); err != nil { - panic(err) - } - - return p -} From cc3fef1a9c4f4d054d7e3475365afa083bf90556 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Sat, 7 Dec 2024 21:01:31 -0800 Subject: [PATCH 10/12] dashes and line width Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 7dc6d73d420..c364289334e 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -202,26 +202,31 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] if err != nil { panic(err) } + if index > 0 { + readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + } readLine.Color = plotutil.Color(index * 2) + readLine.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) s.Color = readLine.Color - s.Shape = plotutil.Shape(i) p.Add(readLine, s) writeLine, s, err := plotter.NewLinePoints(writePts) if err != nil { panic(err) } + if index > 0 { + writeLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + } writeLine.Color = plotutil.Color(index*2 + 1) + writeLine.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) s.Color = writeLine.Color - s.Shape = plotutil.Shape(i) p.Add(writeLine, s) if index == 0 { - sc, _ := plotter.NewScatter(writePts) - sc.Color = color.RGBA{0, 0, 0, 255} - sc.Shape = s.Shape - sc.XYs = s.XYs - legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(sc)) + l, _, _ := plotter.NewLinePoints(writePts) + l.Color = color.RGBA{0, 0, 0, 255} + l.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) + legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(l)) } if i == len(values)-1 { legend.Add(fmt.Sprintf("read %s", fileName), plot.Thumbnailer(readLine)) From de32de65213cff8d4660b1362544d7052cfe2703 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Mon, 9 Dec 2024 22:10:33 -0800 Subject: [PATCH 11/12] second dataset with dashes Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index c364289334e..52bdca58618 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -205,8 +205,8 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] if index > 0 { readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} } - readLine.Color = plotutil.Color(index * 2) - readLine.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) + readLine.Color = plotutil.Color(0) + readLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1)) s.Color = readLine.Color p.Add(readLine, s) @@ -217,15 +217,15 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] if index > 0 { writeLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} } - writeLine.Color = plotutil.Color(index*2 + 1) - writeLine.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) + writeLine.Color = plotutil.Color(2) + writeLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1)) s.Color = writeLine.Color p.Add(writeLine, s) if index == 0 { l, _, _ := plotter.NewLinePoints(writePts) l.Color = color.RGBA{0, 0, 0, 255} - l.Width = vg.Length(vg.Millimeter * 0.1 * vg.Length(i+1)) + l.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1)) legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(l)) } if i == len(values)-1 { From aed1d42cd9b180dc36ddc7c83bdc889e33bcb242 Mon Sep 17 00:00:00 2001 From: Ivan Valdes Date: Tue, 10 Dec 2024 21:15:04 -0800 Subject: [PATCH 12/12] option 2 Signed-off-by: Ivan Valdes --- tools/rw-heatmaps/pkg/chart/line_chart.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go index 52bdca58618..2415793e09c 100644 --- a/tools/rw-heatmaps/pkg/chart/line_chart.go +++ b/tools/rw-heatmaps/pkg/chart/line_chart.go @@ -202,11 +202,13 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] if err != nil { panic(err) } - if index > 0 { - readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + if index == 0 { + readLine.Color = plotutil.Color(0) + } else { + readLine.Color = plotutil.Color(2) } - readLine.Color = plotutil.Color(0) readLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1)) + readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} s.Color = readLine.Color p.Add(readLine, s) @@ -214,10 +216,11 @@ func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][] if err != nil { panic(err) } - if index > 0 { - writeLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)} + if index == 0 { + writeLine.Color = plotutil.Color(0) + } else { + writeLine.Color = plotutil.Color(2) } - writeLine.Color = plotutil.Color(2) writeLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1)) s.Color = writeLine.Color p.Add(writeLine, s)