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  }