go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/pkg/funcs/timeseries_chart.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package funcs 9 10 import ( 11 "bytes" 12 "context" 13 "fmt" 14 "time" 15 16 "github.com/wcharczuk/go-chart/v2" 17 "github.com/wcharczuk/go-chart/v2/drawing" 18 "go.charczuk.com/projects/nodes/pkg/types" 19 "go.charczuk.com/sdk/errutil" 20 "go.charczuk.com/sdk/iter" 21 ) 22 23 // TimeseriesChart renders a chart to svg. 24 func TimeseriesChart(config ChartConfig) func(context.Context, []time.Time, []float64) (types.SVG, error) { 25 return func(ctx context.Context, xValues []time.Time, yValues []float64) (types.SVG, error) { 26 if len(xValues) < 2 { 27 return types.SVG{}, nil 28 } 29 if len(xValues) != len(yValues) { 30 return types.SVG{}, fmt.Errorf("mismatched x-values and y-values length(s)") 31 } 32 main, err := timeseriesChartMain(ctx, config, xValues, yValues) 33 if err != nil { 34 return types.SVG{}, err 35 } 36 thumbnail, err := timeseriesChartThumbnail(ctx, config, xValues, yValues) 37 if err != nil { 38 return types.SVG{}, err 39 } 40 return types.SVG{ 41 Main: main, 42 Thumbnail: thumbnail, 43 }, nil 44 } 45 } 46 47 func timeseriesChartMain[T MathScalars](ctx context.Context, config ChartConfig, xValues []time.Time, yValues []T) (string, error) { 48 var series []chart.Series 49 primarySeries := chart.TimeSeries{ 50 Style: chart.Style{ 51 StrokeColor: drawing.ColorFromHex("abb3bf"), 52 FillColor: chart.ColorBlue.WithAlpha(100), 53 }, 54 XValues: xValues, 55 YValues: iter.Apply(yValues, func(v T) float64 { return float64(v) }), 56 } 57 58 config.ApplyPrimaryTimeSeries(&primarySeries) 59 60 if !config.HideSeries { 61 // we have to create the primary series regardless 62 // if there is a chance we will need it for the regression 63 series = append(series, primarySeries) 64 if !config.HideSeriesLastValue { 65 primarySeriesLastValue := chart.LastValueAnnotationSeries(primarySeries) 66 primarySeriesLastValue.Style.FillColor = drawing.ColorFromHex("252a31") 67 primarySeriesLastValue.Style.StrokeColor = drawing.ColorFromHex("abb3bf") 68 primarySeriesLastValue.Style.FontColor = drawing.ColorFromHex("abb3bf") 69 series = append(series, primarySeriesLastValue) 70 } 71 } 72 if !config.HideRegression { 73 if len(yValues) > 2 { 74 linRegSeries := &chart.LinearRegressionSeries{ 75 Style: chart.Style{ 76 StrokeColor: drawing.ColorFromHex("8E292C"), 77 }, 78 InnerSeries: primarySeries, 79 } 80 series = append(series, linRegSeries) 81 if !config.HideRegressionLastValue { 82 linearRegLastValue := chart.LastValueAnnotationSeries(linRegSeries) 83 linearRegLastValue.Style.FillColor = drawing.ColorFromHex("252a31") 84 linearRegLastValue.Style.StrokeColor = drawing.ColorFromHex("8E292C") 85 linearRegLastValue.Style.FontColor = drawing.ColorFromHex("abb3bf") 86 series = append(series, linearRegLastValue) 87 } 88 } 89 } 90 91 c := chart.Chart{ 92 Log: getChartLoggerForContext(ctx), 93 Width: 720, 94 Height: 405, 95 XAxis: chart.XAxis{ 96 TickStyle: chart.Style{ 97 StrokeColor: drawing.ColorFromHex("383e47"), 98 FontColor: drawing.ColorFromHex("abb3bf"), 99 }, 100 }, 101 YAxis: chart.YAxis{ 102 TickStyle: chart.Style{ 103 StrokeColor: drawing.ColorFromHex("383e47"), 104 FontColor: drawing.ColorFromHex("abb3bf"), 105 }, 106 }, 107 YAxisSecondary: chart.HideYAxis(), 108 Canvas: chart.Style{ 109 FillColor: drawing.ColorTransparent, 110 }, 111 Background: chart.Style{ 112 FillColor: drawing.ColorTransparent, 113 }, 114 Series: series, 115 } 116 config.ApplyChart(&c) 117 buf := new(bytes.Buffer) 118 if err := c.Render(chart.SVG, buf); err != nil { 119 return "", errutil.New(err) 120 } 121 return buf.String(), nil 122 } 123 124 func timeseriesChartThumbnail[T MathScalars](ctx context.Context, config ChartConfig, xValues []time.Time, yValues []T) (string, error) { 125 primarySeries := chart.TimeSeries{ 126 Style: chart.Style{ 127 StrokeColor: drawing.ColorFromHex("abb3bf"), 128 FillColor: chart.ColorBlue.WithAlpha(100), 129 }, 130 XValues: xValues, 131 YValues: iter.Apply(yValues, func(v T) float64 { return float64(v) }), 132 } 133 config.ApplyPrimaryTimeSeries(&primarySeries) 134 135 c := chart.Chart{ 136 Width: 300, 137 Height: 100, 138 Log: getChartLoggerForContext(ctx), 139 XAxis: chart.XAxis{ 140 Style: chart.Hidden(), 141 TickStyle: chart.Hidden(), 142 }, 143 YAxis: chart.YAxis{ 144 Style: chart.Hidden(), 145 TickStyle: chart.Hidden(), 146 }, 147 YAxisSecondary: chart.HideYAxis(), 148 Canvas: chart.Style{ 149 Padding: chart.BoxZero, 150 FillColor: drawing.ColorTransparent, 151 }, 152 Background: chart.Style{ 153 Padding: chart.BoxZero, 154 FillColor: drawing.ColorTransparent, 155 }, 156 Series: []chart.Series{ 157 primarySeries, 158 }, 159 } 160 config.ApplyChartThumbnail(&c) 161 162 buf := new(bytes.Buffer) 163 if err := c.Render(chart.SVG, buf); err != nil { 164 return "", errutil.New(err) 165 } 166 return buf.String(), nil 167 }