├── .github └── workflows │ └── ci.yml ├── .gitignore ├── COVERAGE ├── LICENSE ├── Makefile ├── PROFANITY_RULES.yml ├── README.md ├── _colors └── colors_extended.txt ├── _images ├── bar_chart.png ├── goog_ltm.png ├── ma_goog_ltm.png ├── pie_chart.png ├── spy_ltm_bbs.png ├── stacked_bar.png ├── tvix_ltm.png └── two_axis.png ├── annotation_series.go ├── annotation_series_test.go ├── array.go ├── axis.go ├── bar_chart.go ├── bar_chart_test.go ├── bollinger_band_series.go ├── bollinger_band_series_test.go ├── bounded_last_values_annotation_series.go ├── box.go ├── box_test.go ├── chart.go ├── chart_test.go ├── cmd └── chart │ └── main.go ├── colors.go ├── concat_series.go ├── concat_series_test.go ├── continuous_range.go ├── continuous_range_test.go ├── continuous_series.go ├── continuous_series_test.go ├── defaults.go ├── donut_chart.go ├── donut_chart_test.go ├── draw.go ├── drawing ├── README.md ├── color.go ├── color_test.go ├── constants.go ├── curve.go ├── curve_test.go ├── dasher.go ├── demux_flattener.go ├── drawing.go ├── flattener.go ├── free_type_path.go ├── graphic_context.go ├── image_filter.go ├── line.go ├── matrix.go ├── painter.go ├── path.go ├── raster_graphic_context.go ├── stack_graphic_context.go ├── stroker.go ├── text.go ├── transformer.go └── util.go ├── ema_series.go ├── ema_series_test.go ├── examples ├── annotations │ ├── main.go │ └── output.png ├── axes │ ├── main.go │ └── output.png ├── axes_labels │ ├── main.go │ └── output.png ├── bar_chart │ ├── main.go │ └── output.png ├── bar_chart_base_value │ ├── main.go │ └── output.png ├── basic │ ├── main.go │ └── output.png ├── benchmark_line_charts │ ├── main.go │ └── output.png ├── css_classes │ └── main.go ├── custom_formatters │ ├── main.go │ └── output.png ├── custom_padding │ ├── main.go │ └── output.png ├── custom_ranges │ ├── main.go │ └── output.png ├── custom_styles │ ├── main.go │ └── output.png ├── custom_stylesheets │ ├── inlineOutput.svg │ └── main.go ├── custom_ticks │ ├── main.go │ └── output.png ├── descending │ ├── main.go │ └── output.png ├── donut_chart │ ├── main.go │ ├── output.png │ └── reg.svg ├── horizontal_stacked_bar │ ├── main.go │ └── output.png ├── image_writer │ └── main.go ├── legend │ ├── main.go │ └── output.png ├── legend_left │ ├── main.go │ └── output.png ├── linear_regression │ ├── main.go │ └── output.png ├── logarithmic_axes │ ├── main.go │ └── output.png ├── min_max │ ├── main.go │ └── output.png ├── pie_chart │ ├── main.go │ ├── output.png │ └── reg.svg ├── poly_regression │ ├── main.go │ └── output.png ├── request_timings │ ├── main.go │ ├── output.png │ └── requests.csv ├── rerender │ └── main.go ├── scatter │ ├── main.go │ └── output.png ├── simple_moving_average │ ├── main.go │ └── output.png ├── stacked_bar │ ├── main.go │ └── output.png ├── stacked_bar_labels │ ├── main.go │ └── output.png ├── stock_analysis │ ├── main.go │ └── output.png ├── text_rotation │ ├── main.go │ └── output.png ├── timeseries │ ├── main.go │ └── output.png ├── twoaxis │ ├── main.go │ └── output.png └── twopoint │ ├── main.go │ └── output.png ├── fileutil.go ├── first_value_annotation.go ├── first_value_annotation_test.go ├── font.go ├── go.mod ├── go.sum ├── grid_line.go ├── grid_line_test.go ├── histogram_series.go ├── histogram_series_test.go ├── image_writer.go ├── jet.go ├── last_value_annotation_series.go ├── last_value_annotation_series_test.go ├── legend.go ├── legend_test.go ├── linear_coefficient_provider.go ├── linear_regression_series.go ├── linear_regression_series_test.go ├── linear_sequence.go ├── linear_series.go ├── logarithmic_range.go ├── logarithmic_range_test.go ├── logger.go ├── macd_series.go ├── macd_series_test.go ├── mathutil.go ├── matrix ├── matrix.go ├── matrix_test.go ├── regression.go ├── regression_test.go ├── util.go └── vector.go ├── min_max_series.go ├── parse.go ├── percent_change_series.go ├── percent_change_series_test.go ├── pie_chart.go ├── pie_chart_test.go ├── polynomial_regression_series.go ├── polynomial_regression_test.go ├── random_sequence.go ├── range.go ├── raster_renderer.go ├── renderable.go ├── renderer.go ├── renderer_provider.go ├── roboto └── roboto.go ├── seq.go ├── seq_test.go ├── series.go ├── sma_series.go ├── sma_series_test.go ├── stacked_bar_chart.go ├── stringutil.go ├── stringutil_test.go ├── style.go ├── style_test.go ├── testutil └── helpers.go ├── text.go ├── text_test.go ├── tick.go ├── tick_test.go ├── time_series.go ├── time_series_test.go ├── times.go ├── timeutil.go ├── value.go ├── value_buffer.go ├── value_buffer_test.go ├── value_formatter.go ├── value_formatter_provider.go ├── value_formatter_test.go ├── value_provider.go ├── value_test.go ├── vector_renderer.go ├── vector_renderer_test.go ├── viridis.go ├── xaxis.go ├── xaxis_test.go ├── yaxis.go └── yaxis_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | paths: [ "*.go" ] 8 | pull_request: 9 | branches: [ main ] 10 | paths: [ "*.go" ] 11 | 12 | jobs: 13 | ci: 14 | name: "Tests" 15 | runs-on: ubuntu-latest 16 | 17 | env: 18 | GOOS: "linux" 19 | GOARCH: "amd64" 20 | GO111MODULE: "on" 21 | CGO_ENABLED: "0" 22 | 23 | steps: 24 | - name: Set up Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: 1.21 28 | 29 | - name: Check out go-incr 30 | uses: actions/checkout@v3 31 | 32 | - name: Run all tests 33 | run: go test ./... 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # Other 17 | .vscode 18 | .DS_Store 19 | coverage.html 20 | .idea 21 | -------------------------------------------------------------------------------- /COVERAGE: -------------------------------------------------------------------------------- 1 | 29.02 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 William Charczuk. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: new-install test 2 | 3 | new-install: 4 | @go get -v -u ./... 5 | 6 | generate: 7 | @go generate ./... 8 | 9 | test: 10 | @go test ./... -------------------------------------------------------------------------------- /PROFANITY_RULES.yml: -------------------------------------------------------------------------------- 1 | go-sdk: 2 | excludeFiles: [ "*_test.go" ] 3 | importsContain: [ github.com/blend/go-sdk/* ] 4 | description: "please don't use go-sdk in this repo" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-chart 2 | ======== 3 | 4 | This project is archived! 5 | 6 | I originally released this as a way to publish stock charts in slack bots. It was kind of fun at the time! I never anticipated that it would become heavily used, and as often happens with open source, I have a ton of time commitments elsewhere, and can't reasonbly devote enough time to this project to match the usage. 7 | 8 | There have been a number of forks over time, I'd encourage you all to seek those out, or new charting libraries. 9 | 10 | Best, 11 | 12 | - Will 13 | -------------------------------------------------------------------------------- /_images/bar_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/bar_chart.png -------------------------------------------------------------------------------- /_images/goog_ltm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/goog_ltm.png -------------------------------------------------------------------------------- /_images/ma_goog_ltm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/ma_goog_ltm.png -------------------------------------------------------------------------------- /_images/pie_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/pie_chart.png -------------------------------------------------------------------------------- /_images/spy_ltm_bbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/spy_ltm_bbs.png -------------------------------------------------------------------------------- /_images/stacked_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/stacked_bar.png -------------------------------------------------------------------------------- /_images/tvix_ltm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/tvix_ltm.png -------------------------------------------------------------------------------- /_images/two_axis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/_images/two_axis.png -------------------------------------------------------------------------------- /annotation_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Interface Assertions. 9 | var ( 10 | _ Series = (*AnnotationSeries)(nil) 11 | ) 12 | 13 | // AnnotationSeries is a series of labels on the chart. 14 | type AnnotationSeries struct { 15 | Name string 16 | Style Style 17 | YAxis YAxisType 18 | Annotations []Value2 19 | } 20 | 21 | // GetName returns the name of the time series. 22 | func (as AnnotationSeries) GetName() string { 23 | return as.Name 24 | } 25 | 26 | // GetStyle returns the line style. 27 | func (as AnnotationSeries) GetStyle() Style { 28 | return as.Style 29 | } 30 | 31 | // GetYAxis returns which YAxis the series draws on. 32 | func (as AnnotationSeries) GetYAxis() YAxisType { 33 | return as.YAxis 34 | } 35 | 36 | func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style { 37 | return Style{ 38 | FontColor: DefaultTextColor, 39 | Font: defaults.Font, 40 | FillColor: DefaultAnnotationFillColor, 41 | FontSize: DefaultAnnotationFontSize, 42 | StrokeColor: defaults.StrokeColor, 43 | StrokeWidth: defaults.StrokeWidth, 44 | Padding: DefaultAnnotationPadding, 45 | } 46 | } 47 | 48 | // Measure returns a bounds box of the series. 49 | func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box { 50 | box := Box{ 51 | Top: math.MaxInt32, 52 | Left: math.MaxInt32, 53 | Right: 0, 54 | Bottom: 0, 55 | } 56 | if !as.Style.Hidden { 57 | seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) 58 | for _, a := range as.Annotations { 59 | style := a.Style.InheritFrom(seriesStyle) 60 | lx := canvasBox.Left + xrange.Translate(a.XValue) 61 | ly := canvasBox.Bottom - yrange.Translate(a.YValue) 62 | ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label) 63 | box.Top = MinInt(box.Top, ab.Top) 64 | box.Left = MinInt(box.Left, ab.Left) 65 | box.Right = MaxInt(box.Right, ab.Right) 66 | box.Bottom = MaxInt(box.Bottom, ab.Bottom) 67 | } 68 | } 69 | return box 70 | } 71 | 72 | // Render draws the series. 73 | func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 74 | if !as.Style.Hidden { 75 | seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) 76 | for _, a := range as.Annotations { 77 | style := a.Style.InheritFrom(seriesStyle) 78 | lx := canvasBox.Left + xrange.Translate(a.XValue) 79 | ly := canvasBox.Bottom - yrange.Translate(a.YValue) 80 | Draw.Annotation(r, canvasBox, style, lx, ly, a.Label) 81 | } 82 | } 83 | } 84 | 85 | // Validate validates the series. 86 | func (as AnnotationSeries) Validate() error { 87 | if len(as.Annotations) == 0 { 88 | return fmt.Errorf("annotation series requires annotations to be set and not empty") 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /annotation_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/drawing" 8 | "github.com/wcharczuk/go-chart/v2/testutil" 9 | ) 10 | 11 | func TestAnnotationSeriesMeasure(t *testing.T) { 12 | // replaced new assertions helper 13 | 14 | as := AnnotationSeries{ 15 | Annotations: []Value2{ 16 | {XValue: 1.0, YValue: 1.0, Label: "1.0"}, 17 | {XValue: 2.0, YValue: 2.0, Label: "2.0"}, 18 | {XValue: 3.0, YValue: 3.0, Label: "3.0"}, 19 | {XValue: 4.0, YValue: 4.0, Label: "4.0"}, 20 | }, 21 | } 22 | 23 | r, err := PNG(110, 110) 24 | testutil.AssertNil(t, err) 25 | 26 | f, err := GetDefaultFont() 27 | testutil.AssertNil(t, err) 28 | 29 | xrange := &ContinuousRange{ 30 | Min: 1.0, 31 | Max: 4.0, 32 | Domain: 100, 33 | } 34 | yrange := &ContinuousRange{ 35 | Min: 1.0, 36 | Max: 4.0, 37 | Domain: 100, 38 | } 39 | 40 | cb := Box{ 41 | Top: 5, 42 | Left: 5, 43 | Right: 105, 44 | Bottom: 105, 45 | } 46 | sd := Style{ 47 | FontSize: 10.0, 48 | Font: f, 49 | } 50 | 51 | box := as.Measure(r, cb, xrange, yrange, sd) 52 | testutil.AssertFalse(t, box.IsZero()) 53 | testutil.AssertEqual(t, -5.0, box.Top) 54 | testutil.AssertEqual(t, 5.0, box.Left) 55 | testutil.AssertEqual(t, 146.0, box.Right) //the top,left annotation sticks up 5px and out ~44px. 56 | testutil.AssertEqual(t, 115.0, box.Bottom) 57 | } 58 | 59 | func TestAnnotationSeriesRender(t *testing.T) { 60 | // replaced new assertions helper 61 | 62 | as := AnnotationSeries{ 63 | Style: Style{ 64 | FillColor: drawing.ColorWhite, 65 | StrokeColor: drawing.ColorBlack, 66 | }, 67 | Annotations: []Value2{ 68 | {XValue: 1.0, YValue: 1.0, Label: "1.0"}, 69 | {XValue: 2.0, YValue: 2.0, Label: "2.0"}, 70 | {XValue: 3.0, YValue: 3.0, Label: "3.0"}, 71 | {XValue: 4.0, YValue: 4.0, Label: "4.0"}, 72 | }, 73 | } 74 | 75 | r, err := PNG(110, 110) 76 | testutil.AssertNil(t, err) 77 | 78 | f, err := GetDefaultFont() 79 | testutil.AssertNil(t, err) 80 | 81 | xrange := &ContinuousRange{ 82 | Min: 1.0, 83 | Max: 4.0, 84 | Domain: 100, 85 | } 86 | yrange := &ContinuousRange{ 87 | Min: 1.0, 88 | Max: 4.0, 89 | Domain: 100, 90 | } 91 | 92 | cb := Box{ 93 | Top: 5, 94 | Left: 5, 95 | Right: 105, 96 | Bottom: 105, 97 | } 98 | sd := Style{ 99 | FontSize: 10.0, 100 | Font: f, 101 | } 102 | 103 | as.Render(r, cb, xrange, yrange, sd) 104 | 105 | rr, isRaster := r.(*rasterRenderer) 106 | testutil.AssertTrue(t, isRaster) 107 | testutil.AssertNotNil(t, rr) 108 | 109 | c := rr.i.At(38, 70) 110 | converted, isRGBA := color.RGBAModel.Convert(c).(color.RGBA) 111 | testutil.AssertTrue(t, isRGBA) 112 | testutil.AssertEqual(t, 0, converted.R) 113 | testutil.AssertEqual(t, 0, converted.G) 114 | testutil.AssertEqual(t, 0, converted.B) 115 | } 116 | -------------------------------------------------------------------------------- /array.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | var ( 4 | _ Sequence = (*Array)(nil) 5 | ) 6 | 7 | // NewArray returns a new array from a given set of values. 8 | // Array implements Sequence, which allows it to be used with the sequence helpers. 9 | func NewArray(values ...float64) Array { 10 | return Array(values) 11 | } 12 | 13 | // Array is a wrapper for an array of floats that implements `ValuesProvider`. 14 | type Array []float64 15 | 16 | // Len returns the value provider length. 17 | func (a Array) Len() int { 18 | return len(a) 19 | } 20 | 21 | // GetValue returns the value at a given index. 22 | func (a Array) GetValue(index int) float64 { 23 | return a[index] 24 | } 25 | -------------------------------------------------------------------------------- /axis.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // TickPosition is an enumeration of possible tick drawing positions. 4 | type TickPosition int 5 | 6 | const ( 7 | // TickPositionUnset means to use the default tick position. 8 | TickPositionUnset TickPosition = 0 9 | // TickPositionBetweenTicks draws the labels for a tick between the previous and current tick. 10 | TickPositionBetweenTicks TickPosition = 1 11 | // TickPositionUnderTick draws the tick below the tick. 12 | TickPositionUnderTick TickPosition = 2 13 | ) 14 | 15 | // YAxisType is a type of y-axis; it can either be primary or secondary. 16 | type YAxisType int 17 | 18 | const ( 19 | // YAxisPrimary is the primary axis. 20 | YAxisPrimary YAxisType = 0 21 | // YAxisSecondary is the secondary axis. 22 | YAxisSecondary YAxisType = 1 23 | ) 24 | 25 | // Axis is a chart feature detailing what values happen where. 26 | type Axis interface { 27 | GetName() string 28 | SetName(name string) 29 | 30 | GetStyle() Style 31 | SetStyle(style Style) 32 | 33 | GetTicks() []Tick 34 | GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick 35 | 36 | // GenerateGridLines returns the gridlines for the axis. 37 | GetGridLines(ticks []Tick) []GridLine 38 | 39 | // Measure should return an absolute box for the axis. 40 | // This is used when auto-fitting the canvas to the background. 41 | Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box 42 | 43 | // Render renders the axis. 44 | Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) 45 | } 46 | -------------------------------------------------------------------------------- /bollinger_band_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Interface Assertions. 8 | var ( 9 | _ Series = (*BollingerBandsSeries)(nil) 10 | ) 11 | 12 | // BollingerBandsSeries draws bollinger bands for an inner series. 13 | // Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev. 14 | type BollingerBandsSeries struct { 15 | Name string 16 | Style Style 17 | YAxis YAxisType 18 | 19 | Period int 20 | K float64 21 | InnerSeries ValuesProvider 22 | 23 | valueBuffer *ValueBuffer 24 | } 25 | 26 | // GetName returns the name of the time series. 27 | func (bbs BollingerBandsSeries) GetName() string { 28 | return bbs.Name 29 | } 30 | 31 | // GetStyle returns the line style. 32 | func (bbs BollingerBandsSeries) GetStyle() Style { 33 | return bbs.Style 34 | } 35 | 36 | // GetYAxis returns which YAxis the series draws on. 37 | func (bbs BollingerBandsSeries) GetYAxis() YAxisType { 38 | return bbs.YAxis 39 | } 40 | 41 | // GetPeriod returns the window size. 42 | func (bbs BollingerBandsSeries) GetPeriod() int { 43 | if bbs.Period == 0 { 44 | return DefaultSimpleMovingAveragePeriod 45 | } 46 | return bbs.Period 47 | } 48 | 49 | // GetK returns the K value, or the number of standard deviations above and below 50 | // to band the simple moving average with. 51 | // Typical K value is 2.0. 52 | func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 { 53 | if bbs.K == 0 { 54 | if len(defaults) > 0 { 55 | return defaults[0] 56 | } 57 | return 2.0 58 | } 59 | return bbs.K 60 | } 61 | 62 | // Len returns the number of elements in the series. 63 | func (bbs BollingerBandsSeries) Len() int { 64 | return bbs.InnerSeries.Len() 65 | } 66 | 67 | // GetBoundedValues gets the bounded value for the series. 68 | func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) { 69 | if bbs.InnerSeries == nil { 70 | return 71 | } 72 | if bbs.valueBuffer == nil || index == 0 { 73 | bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod()) 74 | } 75 | if bbs.valueBuffer.Len() >= bbs.GetPeriod() { 76 | bbs.valueBuffer.Dequeue() 77 | } 78 | px, py := bbs.InnerSeries.GetValues(index) 79 | bbs.valueBuffer.Enqueue(py) 80 | x = px 81 | 82 | ay := Seq{bbs.valueBuffer}.Average() 83 | std := Seq{bbs.valueBuffer}.StdDev() 84 | 85 | y1 = ay + (bbs.GetK() * std) 86 | y2 = ay - (bbs.GetK() * std) 87 | return 88 | } 89 | 90 | // GetBoundedLastValues returns the last bounded value for the series. 91 | func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) { 92 | if bbs.InnerSeries == nil { 93 | return 94 | } 95 | period := bbs.GetPeriod() 96 | seriesLength := bbs.InnerSeries.Len() 97 | startAt := seriesLength - period 98 | if startAt < 0 { 99 | startAt = 0 100 | } 101 | 102 | vb := NewValueBufferWithCapacity(period) 103 | for index := startAt; index < seriesLength; index++ { 104 | xn, yn := bbs.InnerSeries.GetValues(index) 105 | vb.Enqueue(yn) 106 | x = xn 107 | } 108 | 109 | ay := Seq{vb}.Average() 110 | std := Seq{vb}.StdDev() 111 | 112 | y1 = ay + (bbs.GetK() * std) 113 | y2 = ay - (bbs.GetK() * std) 114 | 115 | return 116 | } 117 | 118 | // Render renders the series. 119 | func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 120 | s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{ 121 | StrokeWidth: 1.0, 122 | StrokeColor: DefaultAxisColor.WithAlpha(64), 123 | FillColor: DefaultAxisColor.WithAlpha(32), 124 | })) 125 | 126 | Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod()) 127 | } 128 | 129 | // Validate validates the series. 130 | func (bbs BollingerBandsSeries) Validate() error { 131 | if bbs.InnerSeries == nil { 132 | return fmt.Errorf("bollinger bands series requires InnerSeries to be set") 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /bollinger_band_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/wcharczuk/go-chart/v2/testutil" 9 | ) 10 | 11 | func TestBollingerBandSeries(t *testing.T) { 12 | // replaced new assertions helper 13 | 14 | s1 := mockValuesProvider{ 15 | X: LinearRange(1.0, 100.0), 16 | Y: RandomValuesWithMax(100, 1024), 17 | } 18 | 19 | bbs := &BollingerBandsSeries{ 20 | InnerSeries: s1, 21 | } 22 | 23 | xvalues := make([]float64, 100) 24 | y1values := make([]float64, 100) 25 | y2values := make([]float64, 100) 26 | 27 | for x := 0; x < 100; x++ { 28 | xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x) 29 | } 30 | 31 | for x := bbs.GetPeriod(); x < 100; x++ { 32 | testutil.AssertTrue(t, y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x])) 33 | } 34 | } 35 | 36 | func TestBollingerBandLastValue(t *testing.T) { 37 | // replaced new assertions helper 38 | 39 | s1 := mockValuesProvider{ 40 | X: LinearRange(1.0, 100.0), 41 | Y: LinearRange(1.0, 100.0), 42 | } 43 | 44 | bbs := &BollingerBandsSeries{ 45 | InnerSeries: s1, 46 | } 47 | 48 | x, y1, y2 := bbs.GetBoundedLastValues() 49 | testutil.AssertEqual(t, 100.0, x) 50 | testutil.AssertEqual(t, 101, math.Floor(y1)) 51 | testutil.AssertEqual(t, 83, math.Floor(y2)) 52 | } 53 | -------------------------------------------------------------------------------- /bounded_last_values_annotation_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | // BoundedLastValuesAnnotationSeries returns a last value annotation series for a bounded values provider. 6 | func BoundedLastValuesAnnotationSeries(innerSeries FullBoundedValuesProvider, vfs ...ValueFormatter) AnnotationSeries { 7 | lvx, lvy1, lvy2 := innerSeries.GetBoundedLastValues() 8 | 9 | var vf ValueFormatter 10 | if len(vfs) > 0 { 11 | vf = vfs[0] 12 | } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { 13 | _, vf = typed.GetValueFormatters() 14 | } else { 15 | vf = FloatValueFormatter 16 | } 17 | 18 | label1 := vf(lvy1) 19 | label2 := vf(lvy2) 20 | 21 | var seriesName string 22 | var seriesStyle Style 23 | if typed, isTyped := innerSeries.(Series); isTyped { 24 | seriesName = fmt.Sprintf("%s - Last Values", typed.GetName()) 25 | seriesStyle = typed.GetStyle() 26 | } 27 | 28 | return AnnotationSeries{ 29 | Name: seriesName, 30 | Style: seriesStyle, 31 | Annotations: []Value2{ 32 | {XValue: lvx, YValue: lvy1, Label: label1}, 33 | {XValue: lvx, YValue: lvy2, Label: label2}, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /concat_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // ConcatSeries is a special type of series that concatenates its `InnerSeries`. 4 | type ConcatSeries []Series 5 | 6 | // Len returns the length of the concatenated set of series. 7 | func (cs ConcatSeries) Len() int { 8 | total := 0 9 | for _, s := range cs { 10 | if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider { 11 | total += typed.Len() 12 | } 13 | } 14 | 15 | return total 16 | } 17 | 18 | // GetValue returns the value at the (meta) index (i.e 0 => totalLen-1) 19 | func (cs ConcatSeries) GetValue(index int) (x, y float64) { 20 | cursor := 0 21 | for _, s := range cs { 22 | if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider { 23 | len := typed.Len() 24 | if index < cursor+len { 25 | x, y = typed.GetValues(index - cursor) //FENCEPOSTS. 26 | return 27 | } 28 | cursor += typed.Len() 29 | } 30 | } 31 | return 32 | } 33 | 34 | // Validate validates the series. 35 | func (cs ConcatSeries) Validate() error { 36 | var err error 37 | for _, s := range cs { 38 | err = s.Validate() 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /concat_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestConcatSeries(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | s1 := ContinuousSeries{ 13 | XValues: LinearRange(1.0, 10.0), 14 | YValues: LinearRange(1.0, 10.0), 15 | } 16 | 17 | s2 := ContinuousSeries{ 18 | XValues: LinearRange(11, 20.0), 19 | YValues: LinearRange(10.0, 1.0), 20 | } 21 | 22 | s3 := ContinuousSeries{ 23 | XValues: LinearRange(21, 30.0), 24 | YValues: LinearRange(1.0, 10.0), 25 | } 26 | 27 | cs := ConcatSeries([]Series{s1, s2, s3}) 28 | testutil.AssertEqual(t, 30, cs.Len()) 29 | 30 | x0, y0 := cs.GetValue(0) 31 | testutil.AssertEqual(t, 1.0, x0) 32 | testutil.AssertEqual(t, 1.0, y0) 33 | 34 | xm, ym := cs.GetValue(19) 35 | testutil.AssertEqual(t, 20.0, xm) 36 | testutil.AssertEqual(t, 1.0, ym) 37 | 38 | xn, yn := cs.GetValue(29) 39 | testutil.AssertEqual(t, 30.0, xn) 40 | testutil.AssertEqual(t, 10.0, yn) 41 | } 42 | -------------------------------------------------------------------------------- /continuous_range.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // ContinuousRange represents a boundary for a set of numbers. 9 | type ContinuousRange struct { 10 | Min float64 11 | Max float64 12 | Domain int 13 | Descending bool 14 | } 15 | 16 | // IsDescending returns if the range is descending. 17 | func (r ContinuousRange) IsDescending() bool { 18 | return r.Descending 19 | } 20 | 21 | // IsZero returns if the ContinuousRange has been set or not. 22 | func (r ContinuousRange) IsZero() bool { 23 | return (r.Min == 0 || math.IsNaN(r.Min)) && 24 | (r.Max == 0 || math.IsNaN(r.Max)) && 25 | r.Domain == 0 26 | } 27 | 28 | // GetMin gets the min value for the continuous range. 29 | func (r ContinuousRange) GetMin() float64 { 30 | return r.Min 31 | } 32 | 33 | // SetMin sets the min value for the continuous range. 34 | func (r *ContinuousRange) SetMin(min float64) { 35 | r.Min = min 36 | } 37 | 38 | // GetMax returns the max value for the continuous range. 39 | func (r ContinuousRange) GetMax() float64 { 40 | return r.Max 41 | } 42 | 43 | // SetMax sets the max value for the continuous range. 44 | func (r *ContinuousRange) SetMax(max float64) { 45 | r.Max = max 46 | } 47 | 48 | // GetDelta returns the difference between the min and max value. 49 | func (r ContinuousRange) GetDelta() float64 { 50 | return r.Max - r.Min 51 | } 52 | 53 | // GetDomain returns the range domain. 54 | func (r ContinuousRange) GetDomain() int { 55 | return r.Domain 56 | } 57 | 58 | // SetDomain sets the range domain. 59 | func (r *ContinuousRange) SetDomain(domain int) { 60 | r.Domain = domain 61 | } 62 | 63 | // String returns a simple string for the ContinuousRange. 64 | func (r ContinuousRange) String() string { 65 | if r.GetDelta() == 0 { 66 | return "ContinuousRange [empty]" 67 | } 68 | return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) 69 | } 70 | 71 | // Translate maps a given value into the ContinuousRange space. 72 | func (r ContinuousRange) Translate(value float64) int { 73 | normalized := value - r.Min 74 | ratio := normalized / r.GetDelta() 75 | 76 | if r.IsDescending() { 77 | return r.Domain - int(math.Ceil(ratio*float64(r.Domain))) 78 | } 79 | 80 | return int(math.Ceil(ratio * float64(r.Domain))) 81 | } 82 | -------------------------------------------------------------------------------- /continuous_range_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestRangeTranslate(t *testing.T) { 10 | // replaced new assertions helper 11 | values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} 12 | r := ContinuousRange{Domain: 1000} 13 | r.Min, r.Max = MinMax(values...) 14 | 15 | // delta = ~7.0 16 | // value = ~5.0 17 | // domain = ~1000 18 | // 5/8 * 1000 ~= 19 | testutil.AssertEqual(t, 0, r.Translate(1.0)) 20 | testutil.AssertEqual(t, 1000, r.Translate(8.0)) 21 | testutil.AssertEqual(t, 572, r.Translate(5.0)) 22 | } 23 | -------------------------------------------------------------------------------- /continuous_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | // Interface Assertions. 6 | var ( 7 | _ Series = (*ContinuousSeries)(nil) 8 | _ FirstValuesProvider = (*ContinuousSeries)(nil) 9 | _ LastValuesProvider = (*ContinuousSeries)(nil) 10 | ) 11 | 12 | // ContinuousSeries represents a line on a chart. 13 | type ContinuousSeries struct { 14 | Name string 15 | Style Style 16 | 17 | YAxis YAxisType 18 | 19 | XValueFormatter ValueFormatter 20 | YValueFormatter ValueFormatter 21 | 22 | XValues []float64 23 | YValues []float64 24 | } 25 | 26 | // GetName returns the name of the time series. 27 | func (cs ContinuousSeries) GetName() string { 28 | return cs.Name 29 | } 30 | 31 | // GetStyle returns the line style. 32 | func (cs ContinuousSeries) GetStyle() Style { 33 | return cs.Style 34 | } 35 | 36 | // Len returns the number of elements in the series. 37 | func (cs ContinuousSeries) Len() int { 38 | return len(cs.XValues) 39 | } 40 | 41 | // GetValues gets the x,y values at a given index. 42 | func (cs ContinuousSeries) GetValues(index int) (float64, float64) { 43 | return cs.XValues[index], cs.YValues[index] 44 | } 45 | 46 | // GetFirstValues gets the first x,y values. 47 | func (cs ContinuousSeries) GetFirstValues() (float64, float64) { 48 | return cs.XValues[0], cs.YValues[0] 49 | } 50 | 51 | // GetLastValues gets the last x,y values. 52 | func (cs ContinuousSeries) GetLastValues() (float64, float64) { 53 | return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1] 54 | } 55 | 56 | // GetValueFormatters returns value formatter defaults for the series. 57 | func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) { 58 | if cs.XValueFormatter != nil { 59 | x = cs.XValueFormatter 60 | } else { 61 | x = FloatValueFormatter 62 | } 63 | if cs.YValueFormatter != nil { 64 | y = cs.YValueFormatter 65 | } else { 66 | y = FloatValueFormatter 67 | } 68 | return 69 | } 70 | 71 | // GetYAxis returns which YAxis the series draws on. 72 | func (cs ContinuousSeries) GetYAxis() YAxisType { 73 | return cs.YAxis 74 | } 75 | 76 | // Render renders the series. 77 | func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 78 | style := cs.Style.InheritFrom(defaults) 79 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs) 80 | } 81 | 82 | // Validate validates the series. 83 | func (cs ContinuousSeries) Validate() error { 84 | if len(cs.XValues) == 0 { 85 | return fmt.Errorf("continuous series; must have xvalues set") 86 | } 87 | 88 | if len(cs.YValues) == 0 { 89 | return fmt.Errorf("continuous series; must have yvalues set") 90 | } 91 | 92 | if len(cs.XValues) != len(cs.YValues) { 93 | return fmt.Errorf("continuous series; must have same length xvalues as yvalues") 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /continuous_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestContinuousSeries(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | cs := ContinuousSeries{ 14 | Name: "Test Series", 15 | XValues: LinearRange(1.0, 10.0), 16 | YValues: LinearRange(1.0, 10.0), 17 | } 18 | 19 | testutil.AssertEqual(t, "Test Series", cs.GetName()) 20 | testutil.AssertEqual(t, 10, cs.Len()) 21 | x0, y0 := cs.GetValues(0) 22 | testutil.AssertEqual(t, 1.0, x0) 23 | testutil.AssertEqual(t, 1.0, y0) 24 | 25 | xn, yn := cs.GetValues(9) 26 | testutil.AssertEqual(t, 10.0, xn) 27 | testutil.AssertEqual(t, 10.0, yn) 28 | 29 | xn, yn = cs.GetLastValues() 30 | testutil.AssertEqual(t, 10.0, xn) 31 | testutil.AssertEqual(t, 10.0, yn) 32 | } 33 | 34 | func TestContinuousSeriesValueFormatter(t *testing.T) { 35 | // replaced new assertions helper 36 | 37 | cs := ContinuousSeries{ 38 | XValueFormatter: func(v interface{}) string { 39 | return fmt.Sprintf("%f foo", v) 40 | }, 41 | YValueFormatter: func(v interface{}) string { 42 | return fmt.Sprintf("%f bar", v) 43 | }, 44 | } 45 | 46 | xf, yf := cs.GetValueFormatters() 47 | testutil.AssertEqual(t, "0.100000 foo", xf(0.1)) 48 | testutil.AssertEqual(t, "0.100000 bar", yf(0.1)) 49 | } 50 | 51 | func TestContinuousSeriesValidate(t *testing.T) { 52 | // replaced new assertions helper 53 | 54 | cs := ContinuousSeries{ 55 | Name: "Test Series", 56 | XValues: LinearRange(1.0, 10.0), 57 | YValues: LinearRange(1.0, 10.0), 58 | } 59 | testutil.AssertNil(t, cs.Validate()) 60 | 61 | cs = ContinuousSeries{ 62 | Name: "Test Series", 63 | XValues: LinearRange(1.0, 10.0), 64 | } 65 | testutil.AssertNotNil(t, cs.Validate()) 66 | 67 | cs = ContinuousSeries{ 68 | Name: "Test Series", 69 | YValues: LinearRange(1.0, 10.0), 70 | } 71 | testutil.AssertNotNil(t, cs.Validate()) 72 | } 73 | -------------------------------------------------------------------------------- /donut_chart_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestDonutChart(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | pie := DonutChart{ 14 | Canvas: Style{ 15 | FillColor: ColorLightGray, 16 | }, 17 | Values: []Value{ 18 | {Value: 10, Label: "Blue"}, 19 | {Value: 9, Label: "Green"}, 20 | {Value: 8, Label: "Gray"}, 21 | {Value: 7, Label: "Orange"}, 22 | {Value: 6, Label: "HEANG"}, 23 | {Value: 5, Label: "??"}, 24 | {Value: 2, Label: "!!"}, 25 | }, 26 | } 27 | 28 | b := bytes.NewBuffer([]byte{}) 29 | pie.Render(PNG, b) 30 | testutil.AssertNotZero(t, b.Len()) 31 | } 32 | 33 | func TestDonutChartDropsZeroValues(t *testing.T) { 34 | // replaced new assertions helper 35 | 36 | pie := DonutChart{ 37 | Canvas: Style{ 38 | FillColor: ColorLightGray, 39 | }, 40 | Values: []Value{ 41 | {Value: 5, Label: "Blue"}, 42 | {Value: 5, Label: "Green"}, 43 | {Value: 0, Label: "Gray"}, 44 | }, 45 | } 46 | 47 | b := bytes.NewBuffer([]byte{}) 48 | err := pie.Render(PNG, b) 49 | testutil.AssertNil(t, err) 50 | } 51 | 52 | func TestDonutChartAllZeroValues(t *testing.T) { 53 | // replaced new assertions helper 54 | 55 | pie := DonutChart{ 56 | Canvas: Style{ 57 | FillColor: ColorLightGray, 58 | }, 59 | Values: []Value{ 60 | {Value: 0, Label: "Blue"}, 61 | {Value: 0, Label: "Green"}, 62 | {Value: 0, Label: "Gray"}, 63 | }, 64 | } 65 | 66 | b := bytes.NewBuffer([]byte{}) 67 | err := pie.Render(PNG, b) 68 | testutil.AssertNotNil(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /drawing/README.md: -------------------------------------------------------------------------------- 1 | go-chart > drawing 2 | ================== 3 | 4 | The bulk of the code in this package is based on [draw2d](https://github.com/llgcode/draw2d), but 5 | with significant modifications to make the APIs more golang friendly and careful about units (points vs. pixels). -------------------------------------------------------------------------------- /drawing/color_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "image/color" 8 | 9 | "github.com/wcharczuk/go-chart/v2/testutil" 10 | ) 11 | 12 | func TestColorFromHex(t *testing.T) { 13 | white := ColorFromHex("FFFFFF") 14 | testutil.AssertEqual(t, ColorWhite, white) 15 | 16 | shortWhite := ColorFromHex("FFF") 17 | testutil.AssertEqual(t, ColorWhite, shortWhite) 18 | 19 | black := ColorFromHex("000000") 20 | testutil.AssertEqual(t, ColorBlack, black) 21 | 22 | shortBlack := ColorFromHex("000") 23 | testutil.AssertEqual(t, ColorBlack, shortBlack) 24 | 25 | red := ColorFromHex("FF0000") 26 | testutil.AssertEqual(t, ColorRed, red) 27 | 28 | shortRed := ColorFromHex("F00") 29 | testutil.AssertEqual(t, ColorRed, shortRed) 30 | 31 | green := ColorFromHex("008000") 32 | testutil.AssertEqual(t, ColorGreen, green) 33 | 34 | // shortGreen := ColorFromHex("0F0") 35 | // testutil.AssertEqual(t, ColorGreen, shortGreen) 36 | 37 | blue := ColorFromHex("0000FF") 38 | testutil.AssertEqual(t, ColorBlue, blue) 39 | 40 | shortBlue := ColorFromHex("00F") 41 | testutil.AssertEqual(t, ColorBlue, shortBlue) 42 | } 43 | 44 | func TestColorFromHex_handlesHash(t *testing.T) { 45 | withHash := ColorFromHex("#FF0000") 46 | testutil.AssertEqual(t, ColorRed, withHash) 47 | 48 | withoutHash := ColorFromHex("#FF0000") 49 | testutil.AssertEqual(t, ColorRed, withoutHash) 50 | } 51 | 52 | func TestColorFromAlphaMixedRGBA(t *testing.T) { 53 | black := ColorFromAlphaMixedRGBA(color.Black.RGBA()) 54 | testutil.AssertTrue(t, black.Equals(ColorBlack), black.String()) 55 | 56 | white := ColorFromAlphaMixedRGBA(color.White.RGBA()) 57 | testutil.AssertTrue(t, white.Equals(ColorWhite), white.String()) 58 | } 59 | 60 | func Test_ColorFromRGBA(t *testing.T) { 61 | value := "rgba(192, 192, 192, 1.0)" 62 | parsed := ColorFromRGBA(value) 63 | testutil.AssertEqual(t, ColorSilver, parsed) 64 | 65 | value = "rgba(192,192,192,1.0)" 66 | parsed = ColorFromRGBA(value) 67 | testutil.AssertEqual(t, ColorSilver, parsed) 68 | 69 | value = "rgba(192,192,192,1.5)" 70 | parsed = ColorFromRGBA(value) 71 | testutil.AssertEqual(t, ColorSilver, parsed) 72 | } 73 | 74 | func TestParseColor(t *testing.T) { 75 | testCases := [...]struct { 76 | Input string 77 | Expected Color 78 | }{ 79 | {"", Color{}}, 80 | {"white", ColorWhite}, 81 | {"WHITE", ColorWhite}, // caps! 82 | {"black", ColorBlack}, 83 | {"red", ColorRed}, 84 | {"green", ColorGreen}, 85 | {"blue", ColorBlue}, 86 | {"silver", ColorSilver}, 87 | {"maroon", ColorMaroon}, 88 | {"purple", ColorPurple}, 89 | {"fuchsia", ColorFuchsia}, 90 | {"lime", ColorLime}, 91 | {"olive", ColorOlive}, 92 | {"yellow", ColorYellow}, 93 | {"navy", ColorNavy}, 94 | {"teal", ColorTeal}, 95 | {"aqua", ColorAqua}, 96 | 97 | {"rgba(192, 192, 192, 1.0)", ColorSilver}, 98 | {"rgba(192,192,192,1.0)", ColorSilver}, 99 | {"rgb(192, 192, 192)", ColorSilver}, 100 | {"rgb(192,192,192)", ColorSilver}, 101 | 102 | {"#FF0000", ColorRed}, 103 | {"#008000", ColorGreen}, 104 | {"#0000FF", ColorBlue}, 105 | {"#F00", ColorRed}, 106 | {"#080", Color{0, 136, 0, 255}}, 107 | {"#00F", ColorBlue}, 108 | } 109 | 110 | for index, tc := range testCases { 111 | actual := ParseColor(tc.Input) 112 | testutil.AssertEqual(t, tc.Expected, actual, fmt.Sprintf("test case: %d -> %s", index, tc.Input)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /drawing/constants.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | const ( 4 | // DefaultDPI is the default image DPI. 5 | DefaultDPI = 96.0 6 | ) 7 | -------------------------------------------------------------------------------- /drawing/curve_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | type point struct { 10 | X, Y float64 11 | } 12 | 13 | type mockLine struct { 14 | inner []point 15 | } 16 | 17 | func (ml *mockLine) LineTo(x, y float64) { 18 | ml.inner = append(ml.inner, point{x, y}) 19 | } 20 | 21 | func (ml mockLine) Len() int { 22 | return len(ml.inner) 23 | } 24 | 25 | func TestTraceQuad(t *testing.T) { 26 | // replaced new assertions helper 27 | 28 | // Quad 29 | // x1, y1, cpx1, cpy2, x2, y2 float64 30 | // do the 9->12 circle segment 31 | quad := []float64{10, 20, 20, 20, 20, 10} 32 | liner := &mockLine{} 33 | TraceQuad(liner, quad, 0.5) 34 | testutil.AssertNotZero(t, liner.Len()) 35 | } 36 | -------------------------------------------------------------------------------- /drawing/dasher.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // NewDashVertexConverter creates a new dash converter. 4 | func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter { 5 | var dasher DashVertexConverter 6 | dasher.dash = dash 7 | dasher.currentDash = 0 8 | dasher.dashOffset = dashOffset 9 | dasher.next = flattener 10 | return &dasher 11 | } 12 | 13 | // DashVertexConverter is a converter for dash vertexes. 14 | type DashVertexConverter struct { 15 | next Flattener 16 | x, y, distance float64 17 | dash []float64 18 | currentDash int 19 | dashOffset float64 20 | } 21 | 22 | // LineTo implements the pathbuilder interface. 23 | func (dasher *DashVertexConverter) LineTo(x, y float64) { 24 | dasher.lineTo(x, y) 25 | } 26 | 27 | // MoveTo implements the pathbuilder interface. 28 | func (dasher *DashVertexConverter) MoveTo(x, y float64) { 29 | dasher.next.MoveTo(x, y) 30 | dasher.x, dasher.y = x, y 31 | dasher.distance = dasher.dashOffset 32 | dasher.currentDash = 0 33 | } 34 | 35 | // LineJoin implements the pathbuilder interface. 36 | func (dasher *DashVertexConverter) LineJoin() { 37 | dasher.next.LineJoin() 38 | } 39 | 40 | // Close implements the pathbuilder interface. 41 | func (dasher *DashVertexConverter) Close() { 42 | dasher.next.Close() 43 | } 44 | 45 | // End implements the pathbuilder interface. 46 | func (dasher *DashVertexConverter) End() { 47 | dasher.next.End() 48 | } 49 | 50 | func (dasher *DashVertexConverter) lineTo(x, y float64) { 51 | rest := dasher.dash[dasher.currentDash] - dasher.distance 52 | for rest < 0 { 53 | dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] 54 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 55 | rest = dasher.dash[dasher.currentDash] - dasher.distance 56 | } 57 | d := distance(dasher.x, dasher.y, x, y) 58 | for d >= rest { 59 | k := rest / d 60 | lx := dasher.x + k*(x-dasher.x) 61 | ly := dasher.y + k*(y-dasher.y) 62 | if dasher.currentDash%2 == 0 { 63 | // line 64 | dasher.next.LineTo(lx, ly) 65 | } else { 66 | // gap 67 | dasher.next.End() 68 | dasher.next.MoveTo(lx, ly) 69 | } 70 | d = d - rest 71 | dasher.x, dasher.y = lx, ly 72 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 73 | rest = dasher.dash[dasher.currentDash] 74 | } 75 | dasher.distance = d 76 | if dasher.currentDash%2 == 0 { 77 | // line 78 | dasher.next.LineTo(x, y) 79 | } else { 80 | // gap 81 | dasher.next.End() 82 | dasher.next.MoveTo(x, y) 83 | } 84 | if dasher.distance >= dasher.dash[dasher.currentDash] { 85 | dasher.distance = dasher.distance - dasher.dash[dasher.currentDash] 86 | dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash) 87 | } 88 | dasher.x, dasher.y = x, y 89 | } 90 | -------------------------------------------------------------------------------- /drawing/demux_flattener.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // DemuxFlattener is a flattener 4 | type DemuxFlattener struct { 5 | Flatteners []Flattener 6 | } 7 | 8 | // MoveTo implements the path builder interface. 9 | func (dc DemuxFlattener) MoveTo(x, y float64) { 10 | for _, flattener := range dc.Flatteners { 11 | flattener.MoveTo(x, y) 12 | } 13 | } 14 | 15 | // LineTo implements the path builder interface. 16 | func (dc DemuxFlattener) LineTo(x, y float64) { 17 | for _, flattener := range dc.Flatteners { 18 | flattener.LineTo(x, y) 19 | } 20 | } 21 | 22 | // LineJoin implements the path builder interface. 23 | func (dc DemuxFlattener) LineJoin() { 24 | for _, flattener := range dc.Flatteners { 25 | flattener.LineJoin() 26 | } 27 | } 28 | 29 | // Close implements the path builder interface. 30 | func (dc DemuxFlattener) Close() { 31 | for _, flattener := range dc.Flatteners { 32 | flattener.Close() 33 | } 34 | } 35 | 36 | // End implements the path builder interface. 37 | func (dc DemuxFlattener) End() { 38 | for _, flattener := range dc.Flatteners { 39 | flattener.End() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /drawing/flattener.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // Liner receive segment definition 4 | type Liner interface { 5 | // LineTo Draw a line from the current position to the point (x, y) 6 | LineTo(x, y float64) 7 | } 8 | 9 | // Flattener receive segment definition 10 | type Flattener interface { 11 | // MoveTo Start a New line from the point (x, y) 12 | MoveTo(x, y float64) 13 | // LineTo Draw a line from the current position to the point (x, y) 14 | LineTo(x, y float64) 15 | // LineJoin add the most recent starting point to close the path to create a polygon 16 | LineJoin() 17 | // Close add the most recent starting point to close the path to create a polygon 18 | Close() 19 | // End mark the current line as finished so we can draw caps 20 | End() 21 | } 22 | 23 | // Flatten convert curves into straight segments keeping join segments info 24 | func Flatten(path *Path, flattener Flattener, scale float64) { 25 | // First Point 26 | var startX, startY float64 27 | // Current Point 28 | var x, y float64 29 | var i int 30 | for _, cmp := range path.Components { 31 | switch cmp { 32 | case MoveToComponent: 33 | x, y = path.Points[i], path.Points[i+1] 34 | startX, startY = x, y 35 | if i != 0 { 36 | flattener.End() 37 | } 38 | flattener.MoveTo(x, y) 39 | i += 2 40 | case LineToComponent: 41 | x, y = path.Points[i], path.Points[i+1] 42 | flattener.LineTo(x, y) 43 | flattener.LineJoin() 44 | i += 2 45 | case QuadCurveToComponent: 46 | // we include the previous point for the start of the curve 47 | TraceQuad(flattener, path.Points[i-2:], 0.5) 48 | x, y = path.Points[i+2], path.Points[i+3] 49 | flattener.LineTo(x, y) 50 | i += 4 51 | case CubicCurveToComponent: 52 | TraceCubic(flattener, path.Points[i-2:], 0.5) 53 | x, y = path.Points[i+4], path.Points[i+5] 54 | flattener.LineTo(x, y) 55 | i += 6 56 | case ArcToComponent: 57 | x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale) 58 | flattener.LineTo(x, y) 59 | i += 6 60 | case CloseComponent: 61 | flattener.LineTo(startX, startY) 62 | flattener.Close() 63 | } 64 | } 65 | flattener.End() 66 | } 67 | 68 | // SegmentedPath is a path of disparate point sectinos. 69 | type SegmentedPath struct { 70 | Points []float64 71 | } 72 | 73 | // MoveTo implements the path interface. 74 | func (p *SegmentedPath) MoveTo(x, y float64) { 75 | p.Points = append(p.Points, x, y) 76 | // TODO need to mark this point as moveto 77 | } 78 | 79 | // LineTo implements the path interface. 80 | func (p *SegmentedPath) LineTo(x, y float64) { 81 | p.Points = append(p.Points, x, y) 82 | } 83 | 84 | // LineJoin implements the path interface. 85 | func (p *SegmentedPath) LineJoin() { 86 | // TODO need to mark the current point as linejoin 87 | } 88 | 89 | // Close implements the path interface. 90 | func (p *SegmentedPath) Close() { 91 | // TODO Close 92 | } 93 | 94 | // End implements the path interface. 95 | func (p *SegmentedPath) End() { 96 | // Nothing to do 97 | } 98 | -------------------------------------------------------------------------------- /drawing/free_type_path.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "github.com/golang/freetype/raster" 5 | "golang.org/x/image/math/fixed" 6 | ) 7 | 8 | // FtLineBuilder is a builder for freetype raster glyphs. 9 | type FtLineBuilder struct { 10 | Adder raster.Adder 11 | } 12 | 13 | // MoveTo implements the path builder interface. 14 | func (liner FtLineBuilder) MoveTo(x, y float64) { 15 | liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) 16 | } 17 | 18 | // LineTo implements the path builder interface. 19 | func (liner FtLineBuilder) LineTo(x, y float64) { 20 | liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}) 21 | } 22 | 23 | // LineJoin implements the path builder interface. 24 | func (liner FtLineBuilder) LineJoin() {} 25 | 26 | // Close implements the path builder interface. 27 | func (liner FtLineBuilder) Close() {} 28 | 29 | // End implements the path builder interface. 30 | func (liner FtLineBuilder) End() {} 31 | -------------------------------------------------------------------------------- /drawing/graphic_context.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "github.com/golang/freetype/truetype" 8 | ) 9 | 10 | // GraphicContext describes the interface for the various backends (images, pdf, opengl, ...) 11 | type GraphicContext interface { 12 | // PathBuilder describes the interface for path drawing 13 | PathBuilder 14 | // BeginPath creates a new path 15 | BeginPath() 16 | // GetMatrixTransform returns the current transformation matrix 17 | GetMatrixTransform() Matrix 18 | // SetMatrixTransform sets the current transformation matrix 19 | SetMatrixTransform(tr Matrix) 20 | // ComposeMatrixTransform composes the current transformation matrix with tr 21 | ComposeMatrixTransform(tr Matrix) 22 | // Rotate applies a rotation to the current transformation matrix. angle is in radian. 23 | Rotate(angle float64) 24 | // Translate applies a translation to the current transformation matrix. 25 | Translate(tx, ty float64) 26 | // Scale applies a scale to the current transformation matrix. 27 | Scale(sx, sy float64) 28 | // SetStrokeColor sets the current stroke color 29 | SetStrokeColor(c color.Color) 30 | // SetFillColor sets the current fill color 31 | SetFillColor(c color.Color) 32 | // SetFillRule sets the current fill rule 33 | SetFillRule(f FillRule) 34 | // SetLineWidth sets the current line width 35 | SetLineWidth(lineWidth float64) 36 | // SetLineCap sets the current line cap 37 | SetLineCap(cap LineCap) 38 | // SetLineJoin sets the current line join 39 | SetLineJoin(join LineJoin) 40 | // SetLineDash sets the current dash 41 | SetLineDash(dash []float64, dashOffset float64) 42 | // SetFontSize sets the current font size 43 | SetFontSize(fontSize float64) 44 | // GetFontSize gets the current font size 45 | GetFontSize() float64 46 | // SetFont sets the font for the context 47 | SetFont(f *truetype.Font) 48 | // GetFont returns the current font 49 | GetFont() *truetype.Font 50 | // DrawImage draws the raster image in the current canvas 51 | DrawImage(image image.Image) 52 | // Save the context and push it to the context stack 53 | Save() 54 | // Restore remove the current context and restore the last one 55 | Restore() 56 | // Clear fills the current canvas with a default transparent color 57 | Clear() 58 | // ClearRect fills the specified rectangle with a default transparent color 59 | ClearRect(x1, y1, x2, y2 int) 60 | // SetDPI sets the current DPI 61 | SetDPI(dpi int) 62 | // GetDPI gets the current DPI 63 | GetDPI() int 64 | // GetStringBounds gets pixel bounds(dimensions) of given string 65 | GetStringBounds(s string) (left, top, right, bottom float64) 66 | // CreateStringPath creates a path from the string s at x, y 67 | CreateStringPath(text string, x, y float64) (cursor float64) 68 | // FillString draws the text at point (0, 0) 69 | FillString(text string) (cursor float64) 70 | // FillStringAt draws the text at the specified point (x, y) 71 | FillStringAt(text string, x, y float64) (cursor float64) 72 | // StrokeString draws the contour of the text at point (0, 0) 73 | StrokeString(text string) (cursor float64) 74 | // StrokeStringAt draws the contour of the text at point (x, y) 75 | StrokeStringAt(text string, x, y float64) (cursor float64) 76 | // Stroke strokes the paths with the color specified by SetStrokeColor 77 | Stroke(paths ...*Path) 78 | // Fill fills the paths with the color specified by SetFillColor 79 | Fill(paths ...*Path) 80 | // FillStroke first fills the paths and than strokes them 81 | FillStroke(paths ...*Path) 82 | } 83 | -------------------------------------------------------------------------------- /drawing/image_filter.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // ImageFilter defines the type of filter to use 4 | type ImageFilter int 5 | 6 | const ( 7 | // LinearFilter defines a linear filter 8 | LinearFilter ImageFilter = iota 9 | // BilinearFilter defines a bilinear filter 10 | BilinearFilter 11 | // BicubicFilter defines a bicubic filter 12 | BicubicFilter 13 | ) 14 | -------------------------------------------------------------------------------- /drawing/line.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "image/color" 5 | "image/draw" 6 | ) 7 | 8 | // PolylineBresenham draws a polyline to an image 9 | func PolylineBresenham(img draw.Image, c color.Color, s ...float64) { 10 | for i := 2; i < len(s); i += 2 { 11 | Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5)) 12 | } 13 | } 14 | 15 | // Bresenham draws a line between (x0, y0) and (x1, y1) 16 | func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) { 17 | dx := abs(x1 - x0) 18 | dy := abs(y1 - y0) 19 | var sx, sy int 20 | if x0 < x1 { 21 | sx = 1 22 | } else { 23 | sx = -1 24 | } 25 | if y0 < y1 { 26 | sy = 1 27 | } else { 28 | sy = -1 29 | } 30 | err := dx - dy 31 | 32 | var e2 int 33 | for { 34 | img.Set(x0, y0, color) 35 | if x0 == x1 && y0 == y1 { 36 | return 37 | } 38 | e2 = 2 * err 39 | if e2 > -dy { 40 | err = err - dy 41 | x0 = x0 + sx 42 | } 43 | if e2 < dx { 44 | err = err + dx 45 | y0 = y0 + sy 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /drawing/painter.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "golang.org/x/image/draw" 8 | "golang.org/x/image/math/f64" 9 | 10 | "github.com/golang/freetype/raster" 11 | ) 12 | 13 | // Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter 14 | type Painter interface { 15 | raster.Painter 16 | SetColor(color color.Color) 17 | } 18 | 19 | // DrawImage draws an image into dest using an affine transformation matrix, an op and a filter 20 | func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) { 21 | var transformer draw.Transformer 22 | switch filter { 23 | case LinearFilter: 24 | transformer = draw.NearestNeighbor 25 | case BilinearFilter: 26 | transformer = draw.BiLinear 27 | case BicubicFilter: 28 | transformer = draw.CatmullRom 29 | } 30 | transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil) 31 | } 32 | -------------------------------------------------------------------------------- /drawing/stroker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package drawing 5 | 6 | // NewLineStroker creates a new line stroker. 7 | func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker { 8 | l := new(LineStroker) 9 | l.Flattener = flattener 10 | l.HalfLineWidth = 0.5 11 | l.Cap = c 12 | l.Join = j 13 | return l 14 | } 15 | 16 | // LineStroker draws the stroke portion of a line. 17 | type LineStroker struct { 18 | Flattener Flattener 19 | HalfLineWidth float64 20 | Cap LineCap 21 | Join LineJoin 22 | vertices []float64 23 | rewind []float64 24 | x, y, nx, ny float64 25 | } 26 | 27 | // MoveTo implements the path builder interface. 28 | func (l *LineStroker) MoveTo(x, y float64) { 29 | l.x, l.y = x, y 30 | } 31 | 32 | // LineTo implements the path builder interface. 33 | func (l *LineStroker) LineTo(x, y float64) { 34 | l.line(l.x, l.y, x, y) 35 | } 36 | 37 | // LineJoin implements the path builder interface. 38 | func (l *LineStroker) LineJoin() {} 39 | 40 | func (l *LineStroker) line(x1, y1, x2, y2 float64) { 41 | dx := (x2 - x1) 42 | dy := (y2 - y1) 43 | d := vectorDistance(dx, dy) 44 | if d != 0 { 45 | nx := dy * l.HalfLineWidth / d 46 | ny := -(dx * l.HalfLineWidth / d) 47 | l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny) 48 | l.x, l.y, l.nx, l.ny = x2, y2, nx, ny 49 | } 50 | } 51 | 52 | // Close implements the path builder interface. 53 | func (l *LineStroker) Close() { 54 | if len(l.vertices) > 1 { 55 | l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1]) 56 | } 57 | } 58 | 59 | // End implements the path builder interface. 60 | func (l *LineStroker) End() { 61 | if len(l.vertices) > 1 { 62 | l.Flattener.MoveTo(l.vertices[0], l.vertices[1]) 63 | for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 { 64 | l.Flattener.LineTo(l.vertices[i], l.vertices[j]) 65 | } 66 | } 67 | for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 { 68 | l.Flattener.LineTo(l.rewind[i], l.rewind[j]) 69 | } 70 | if len(l.vertices) > 1 { 71 | l.Flattener.LineTo(l.vertices[0], l.vertices[1]) 72 | } 73 | l.Flattener.End() 74 | // reinit vertices 75 | l.vertices = l.vertices[0:0] 76 | l.rewind = l.rewind[0:0] 77 | l.x, l.y, l.nx, l.ny = 0, 0, 0, 0 78 | 79 | } 80 | 81 | func (l *LineStroker) appendVertex(vertices ...float64) { 82 | s := len(vertices) / 2 83 | l.vertices = append(l.vertices, vertices[:s]...) 84 | l.rewind = append(l.rewind, vertices[s:]...) 85 | } 86 | -------------------------------------------------------------------------------- /drawing/text.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | "golang.org/x/image/math/fixed" 6 | ) 7 | 8 | // DrawContour draws the given closed contour at the given sub-pixel offset. 9 | func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) { 10 | if len(ps) == 0 { 11 | return 12 | } 13 | startX, startY := pointToF64Point(ps[0]) 14 | path.MoveTo(startX+dx, startY+dy) 15 | q0X, q0Y, on0 := startX, startY, true 16 | for _, p := range ps[1:] { 17 | qX, qY := pointToF64Point(p) 18 | on := p.Flags&0x01 != 0 19 | if on { 20 | if on0 { 21 | path.LineTo(qX+dx, qY+dy) 22 | } else { 23 | path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) 24 | } 25 | } else if !on0 { 26 | midX := (q0X + qX) / 2 27 | midY := (q0Y + qY) / 2 28 | path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) 29 | } 30 | q0X, q0Y, on0 = qX, qY, on 31 | } 32 | // Close the curve. 33 | if on0 { 34 | path.LineTo(startX+dx, startY+dy) 35 | } else { 36 | path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy) 37 | } 38 | } 39 | 40 | // FontExtents contains font metric information. 41 | type FontExtents struct { 42 | // Ascent is the distance that the text 43 | // extends above the baseline. 44 | Ascent float64 45 | 46 | // Descent is the distance that the text 47 | // extends below the baseline. The descent 48 | // is given as a negative value. 49 | Descent float64 50 | 51 | // Height is the distance from the lowest 52 | // descending point to the highest ascending 53 | // point. 54 | Height float64 55 | } 56 | 57 | // Extents returns the FontExtents for a font. 58 | // TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro 59 | func Extents(font *truetype.Font, size float64) FontExtents { 60 | bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm())) 61 | scale := size / float64(font.FUnitsPerEm()) 62 | return FontExtents{ 63 | Ascent: float64(bounds.Max.Y) * scale, 64 | Descent: float64(bounds.Min.Y) * scale, 65 | Height: float64(bounds.Max.Y-bounds.Min.Y) * scale, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /drawing/transformer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The draw2d Authors. All rights reserved. 2 | // created: 13/12/2010 by Laurent Le Goff 3 | 4 | package drawing 5 | 6 | // Transformer apply the Matrix transformation tr 7 | type Transformer struct { 8 | Tr Matrix 9 | Flattener Flattener 10 | } 11 | 12 | // MoveTo implements the path builder interface. 13 | func (t Transformer) MoveTo(x, y float64) { 14 | u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] 15 | v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] 16 | t.Flattener.MoveTo(u, v) 17 | } 18 | 19 | // LineTo implements the path builder interface. 20 | func (t Transformer) LineTo(x, y float64) { 21 | u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4] 22 | v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5] 23 | t.Flattener.LineTo(u, v) 24 | } 25 | 26 | // LineJoin implements the path builder interface. 27 | func (t Transformer) LineJoin() { 28 | t.Flattener.LineJoin() 29 | } 30 | 31 | // Close implements the path builder interface. 32 | func (t Transformer) Close() { 33 | t.Flattener.Close() 34 | } 35 | 36 | // End implements the path builder interface. 37 | func (t Transformer) End() { 38 | t.Flattener.End() 39 | } 40 | -------------------------------------------------------------------------------- /drawing/util.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "math" 5 | 6 | "golang.org/x/image/math/fixed" 7 | 8 | "github.com/golang/freetype/raster" 9 | "github.com/golang/freetype/truetype" 10 | ) 11 | 12 | // PixelsToPoints returns the points for a given number of pixels at a DPI. 13 | func PixelsToPoints(dpi, pixels float64) (points float64) { 14 | points = (pixels * 72.0) / dpi 15 | return 16 | } 17 | 18 | // PointsToPixels returns the pixels for a given number of points at a DPI. 19 | func PointsToPixels(dpi, points float64) (pixels float64) { 20 | pixels = (points * dpi) / 72.0 21 | return 22 | } 23 | 24 | func abs(i int) int { 25 | if i < 0 { 26 | return -i 27 | } 28 | return i 29 | } 30 | 31 | func distance(x1, y1, x2, y2 float64) float64 { 32 | return vectorDistance(x2-x1, y2-y1) 33 | } 34 | 35 | func vectorDistance(dx, dy float64) float64 { 36 | return float64(math.Sqrt(dx*dx + dy*dy)) 37 | } 38 | 39 | func toFtCap(c LineCap) raster.Capper { 40 | switch c { 41 | case RoundCap: 42 | return raster.RoundCapper 43 | case ButtCap: 44 | return raster.ButtCapper 45 | case SquareCap: 46 | return raster.SquareCapper 47 | } 48 | return raster.RoundCapper 49 | } 50 | 51 | func toFtJoin(j LineJoin) raster.Joiner { 52 | switch j { 53 | case RoundJoin: 54 | return raster.RoundJoiner 55 | case BevelJoin: 56 | return raster.BevelJoiner 57 | } 58 | return raster.RoundJoiner 59 | } 60 | 61 | func pointToF64Point(p truetype.Point) (x, y float64) { 62 | return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) 63 | } 64 | 65 | func fUnitsToFloat64(x fixed.Int26_6) float64 { 66 | scaled := x << 2 67 | return float64(scaled/256) + float64(scaled%256)/256.0 68 | } 69 | -------------------------------------------------------------------------------- /ema_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // DefaultEMAPeriod is the default EMA period used in the sigma calculation. 7 | DefaultEMAPeriod = 12 8 | ) 9 | 10 | // Interface Assertions. 11 | var ( 12 | _ Series = (*EMASeries)(nil) 13 | _ FirstValuesProvider = (*EMASeries)(nil) 14 | _ LastValuesProvider = (*EMASeries)(nil) 15 | ) 16 | 17 | // EMASeries is a computed series. 18 | type EMASeries struct { 19 | Name string 20 | Style Style 21 | YAxis YAxisType 22 | 23 | Period int 24 | InnerSeries ValuesProvider 25 | 26 | cache []float64 27 | } 28 | 29 | // GetName returns the name of the time series. 30 | func (ema EMASeries) GetName() string { 31 | return ema.Name 32 | } 33 | 34 | // GetStyle returns the line style. 35 | func (ema EMASeries) GetStyle() Style { 36 | return ema.Style 37 | } 38 | 39 | // GetYAxis returns which YAxis the series draws on. 40 | func (ema EMASeries) GetYAxis() YAxisType { 41 | return ema.YAxis 42 | } 43 | 44 | // GetPeriod returns the window size. 45 | func (ema EMASeries) GetPeriod() int { 46 | if ema.Period == 0 { 47 | return DefaultEMAPeriod 48 | } 49 | return ema.Period 50 | } 51 | 52 | // Len returns the number of elements in the series. 53 | func (ema EMASeries) Len() int { 54 | return ema.InnerSeries.Len() 55 | } 56 | 57 | // GetSigma returns the smoothing factor for the serise. 58 | func (ema EMASeries) GetSigma() float64 { 59 | return 2.0 / (float64(ema.GetPeriod()) + 1) 60 | } 61 | 62 | // GetValues gets a value at a given index. 63 | func (ema *EMASeries) GetValues(index int) (x, y float64) { 64 | if ema.InnerSeries == nil { 65 | return 66 | } 67 | if len(ema.cache) == 0 { 68 | ema.ensureCachedValues() 69 | } 70 | vx, _ := ema.InnerSeries.GetValues(index) 71 | x = vx 72 | y = ema.cache[index] 73 | return 74 | } 75 | 76 | // GetFirstValues computes the first moving average value. 77 | func (ema *EMASeries) GetFirstValues() (x, y float64) { 78 | if ema.InnerSeries == nil { 79 | return 80 | } 81 | if len(ema.cache) == 0 { 82 | ema.ensureCachedValues() 83 | } 84 | x, _ = ema.InnerSeries.GetValues(0) 85 | y = ema.cache[0] 86 | return 87 | } 88 | 89 | // GetLastValues computes the last moving average value but walking back window size samples, 90 | // and recomputing the last moving average chunk. 91 | func (ema *EMASeries) GetLastValues() (x, y float64) { 92 | if ema.InnerSeries == nil { 93 | return 94 | } 95 | if len(ema.cache) == 0 { 96 | ema.ensureCachedValues() 97 | } 98 | lastIndex := ema.InnerSeries.Len() - 1 99 | x, _ = ema.InnerSeries.GetValues(lastIndex) 100 | y = ema.cache[lastIndex] 101 | return 102 | } 103 | 104 | func (ema *EMASeries) ensureCachedValues() { 105 | seriesLength := ema.InnerSeries.Len() 106 | ema.cache = make([]float64, seriesLength) 107 | sigma := ema.GetSigma() 108 | for x := 0; x < seriesLength; x++ { 109 | _, y := ema.InnerSeries.GetValues(x) 110 | if x == 0 { 111 | ema.cache[x] = y 112 | continue 113 | } 114 | previousEMA := ema.cache[x-1] 115 | ema.cache[x] = ((y - previousEMA) * sigma) + previousEMA 116 | } 117 | } 118 | 119 | // Render renders the series. 120 | func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 121 | style := ema.Style.InheritFrom(defaults) 122 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema) 123 | } 124 | 125 | // Validate validates the series. 126 | func (ema *EMASeries) Validate() error { 127 | if ema.InnerSeries == nil { 128 | return fmt.Errorf("ema series requires InnerSeries to be set") 129 | } 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /ema_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | var ( 10 | emaXValues = LinearRange(1.0, 50.0) 11 | emaYValues = []float64{ 12 | 1, 2, 3, 4, 5, 4, 3, 2, 13 | 1, 2, 3, 4, 5, 4, 3, 2, 14 | 1, 2, 3, 4, 5, 4, 3, 2, 15 | 1, 2, 3, 4, 5, 4, 3, 2, 16 | 1, 2, 3, 4, 5, 4, 3, 2, 17 | 1, 2, 3, 4, 5, 4, 3, 2, 18 | 1, 2, 19 | } 20 | emaExpected = []float64{ 21 | 1, 22 | 1.074074074, 23 | 1.216735254, 24 | 1.422903013, 25 | 1.68787316, 26 | 1.859141815, 27 | 1.943649828, 28 | 1.947823915, 29 | 1.877614736, 30 | 1.886680311, 31 | 1.969148437, 32 | 2.119581886, 33 | 2.33294619, 34 | 2.456431658, 35 | 2.496695979, 36 | 2.459903685, 37 | 2.351762671, 38 | 2.325706177, 39 | 2.375653867, 40 | 2.495975803, 41 | 2.681459077, 42 | 2.779128775, 43 | 2.795489607, 44 | 2.73656445, 45 | 2.607930047, 46 | 2.562898191, 47 | 2.595276103, 48 | 2.699329725, 49 | 2.869749746, 50 | 2.953471987, 51 | 2.956918506, 52 | 2.886035654, 53 | 2.746329309, 54 | 2.691045657, 55 | 2.713931163, 56 | 2.809195522, 57 | 2.971477335, 58 | 3.047664199, 59 | 3.044133518, 60 | 2.966790294, 61 | 2.821102124, 62 | 2.760279745, 63 | 2.778036801, 64 | 2.868552593, 65 | 3.026437586, 66 | 3.098553321, 67 | 3.091253075, 68 | 3.010419514, 69 | 2.86149955, 70 | 2.797684768, 71 | } 72 | emaDelta = 0.0001 73 | ) 74 | 75 | func TestEMASeries(t *testing.T) { 76 | // replaced new assertions helper 77 | 78 | mockSeries := mockValuesProvider{ 79 | emaXValues, 80 | emaYValues, 81 | } 82 | testutil.AssertEqual(t, 50, mockSeries.Len()) 83 | 84 | ema := &EMASeries{ 85 | InnerSeries: mockSeries, 86 | Period: 26, 87 | } 88 | 89 | sig := ema.GetSigma() 90 | testutil.AssertEqual(t, 2.0/(26.0+1), sig) 91 | 92 | var yvalues []float64 93 | for x := 0; x < ema.Len(); x++ { 94 | _, y := ema.GetValues(x) 95 | yvalues = append(yvalues, y) 96 | } 97 | 98 | for index, yv := range yvalues { 99 | testutil.AssertInDelta(t, yv, emaExpected[index], emaDelta) 100 | } 101 | 102 | lvx, lvy := ema.GetLastValues() 103 | testutil.AssertEqual(t, 50.0, lvx) 104 | testutil.AssertInDelta(t, lvy, emaExpected[49], emaDelta) 105 | } 106 | -------------------------------------------------------------------------------- /examples/annotations/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we add an `Annotation` series, which is a special type of series that 14 | draws annotation labels at given X and Y values (as translated by their respective ranges). 15 | 16 | It is important to not that the chart automatically sizes the canvas box to fit the annotations, 17 | As well as automatically assign a series color for the `Stroke` or border component of the series. 18 | 19 | The annotation series is most often used by the original author to show the last value of another series, but 20 | they can be used in other capacities as well. 21 | */ 22 | 23 | graph := chart.Chart{ 24 | Series: []chart.Series{ 25 | chart.ContinuousSeries{ 26 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 27 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 28 | }, 29 | chart.AnnotationSeries{ 30 | Annotations: []chart.Value2{ 31 | {XValue: 1.0, YValue: 1.0, Label: "One"}, 32 | {XValue: 2.0, YValue: 2.0, Label: "Two"}, 33 | {XValue: 3.0, YValue: 3.0, Label: "Three"}, 34 | {XValue: 4.0, YValue: 4.0, Label: "Four"}, 35 | {XValue: 5.0, YValue: 5.0, Label: "Five"}, 36 | }, 37 | }, 38 | }, 39 | } 40 | 41 | f, _ := os.Create("output.png") 42 | defer f.Close() 43 | graph.Render(chart.PNG, f) 44 | } 45 | -------------------------------------------------------------------------------- /examples/annotations/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/annotations/output.png -------------------------------------------------------------------------------- /examples/axes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | chart "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 15 | In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip. 16 | */ 17 | 18 | graph := chart.Chart{ 19 | Series: []chart.Series{ 20 | chart.ContinuousSeries{ 21 | Style: chart.Style{ 22 | StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), 23 | FillColor: chart.GetDefaultColor(0).WithAlpha(64), 24 | }, 25 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 26 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 27 | }, 28 | }, 29 | } 30 | 31 | f, _ := os.Create("output.png") 32 | defer f.Close() 33 | graph.Render(chart.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /examples/axes/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/axes/output.png -------------------------------------------------------------------------------- /examples/axes_labels/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | chart "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 15 | In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip. 16 | */ 17 | 18 | graph := chart.Chart{ 19 | XAxis: chart.XAxis{ 20 | Name: "The XAxis", 21 | }, 22 | YAxis: chart.YAxis{ 23 | Name: "The YAxis", 24 | }, 25 | Series: []chart.Series{ 26 | chart.ContinuousSeries{ 27 | Style: chart.Style{ 28 | StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), 29 | FillColor: chart.GetDefaultColor(0).WithAlpha(64), 30 | }, 31 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 32 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 33 | }, 34 | }, 35 | } 36 | 37 | f, _ := os.Create("output.png") 38 | defer f.Close() 39 | graph.Render(chart.PNG, f) 40 | } 41 | -------------------------------------------------------------------------------- /examples/axes_labels/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/axes_labels/output.png -------------------------------------------------------------------------------- /examples/bar_chart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | graph := chart.BarChart{ 13 | Title: "Test Bar Chart", 14 | Background: chart.Style{ 15 | Padding: chart.Box{ 16 | Top: 40, 17 | }, 18 | }, 19 | Height: 512, 20 | BarWidth: 60, 21 | Bars: []chart.Value{ 22 | {Value: 5.25, Label: "Blue"}, 23 | {Value: 4.88, Label: "Green"}, 24 | {Value: 4.74, Label: "Gray"}, 25 | {Value: 3.22, Label: "Orange"}, 26 | {Value: 3, Label: "Test"}, 27 | {Value: 2.27, Label: "??"}, 28 | {Value: 1, Label: "!!"}, 29 | }, 30 | } 31 | 32 | f, _ := os.Create("output.png") 33 | defer f.Close() 34 | graph.Render(chart.PNG, f) 35 | } 36 | -------------------------------------------------------------------------------- /examples/bar_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/bar_chart/output.png -------------------------------------------------------------------------------- /examples/bar_chart_base_value/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | "github.com/wcharczuk/go-chart/v2/drawing" 10 | ) 11 | 12 | func main() { 13 | profitStyle := chart.Style{ 14 | FillColor: drawing.ColorFromHex("13c158"), 15 | StrokeColor: drawing.ColorFromHex("13c158"), 16 | StrokeWidth: 0, 17 | } 18 | 19 | lossStyle := chart.Style{ 20 | FillColor: drawing.ColorFromHex("c11313"), 21 | StrokeColor: drawing.ColorFromHex("c11313"), 22 | StrokeWidth: 0, 23 | } 24 | 25 | sbc := chart.BarChart{ 26 | Title: "Bar Chart Using BaseValue", 27 | Background: chart.Style{ 28 | Padding: chart.Box{ 29 | Top: 40, 30 | }, 31 | }, 32 | Height: 512, 33 | BarWidth: 60, 34 | YAxis: chart.YAxis{ 35 | Ticks: []chart.Tick{ 36 | {Value: -4.0, Label: "-4"}, 37 | {Value: -2.0, Label: "-2"}, 38 | {Value: 0, Label: "0"}, 39 | {Value: 2.0, Label: "2"}, 40 | {Value: 4.0, Label: "4"}, 41 | {Value: 6.0, Label: "6"}, 42 | {Value: 8.0, Label: "8"}, 43 | {Value: 10.0, Label: "10"}, 44 | {Value: 12.0, Label: "12"}, 45 | }, 46 | }, 47 | UseBaseValue: true, 48 | BaseValue: 0.0, 49 | Bars: []chart.Value{ 50 | {Value: 10.0, Style: profitStyle, Label: "Profit"}, 51 | {Value: 12.0, Style: profitStyle, Label: "More Profit"}, 52 | {Value: 8.0, Style: profitStyle, Label: "Still Profit"}, 53 | {Value: -4.0, Style: lossStyle, Label: "Loss!"}, 54 | {Value: 3.0, Style: profitStyle, Label: "Phew Ok"}, 55 | {Value: -2.0, Style: lossStyle, Label: "Oh No!"}, 56 | }, 57 | } 58 | 59 | f, _ := os.Create("output.png") 60 | defer f.Close() 61 | sbc.Render(chart.PNG, f) 62 | } 63 | -------------------------------------------------------------------------------- /examples/bar_chart_base_value/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/bar_chart_base_value/output.png -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | graph := chart.Chart{ 13 | Series: []chart.Series{ 14 | chart.ContinuousSeries{ 15 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 16 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 17 | }, 18 | }, 19 | } 20 | f, _ := os.Create("output.png") 21 | defer f.Close() 22 | graph.Render(chart.PNG, f) 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/basic/output.png -------------------------------------------------------------------------------- /examples/benchmark_line_charts/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "time" 10 | 11 | "github.com/wcharczuk/go-chart/v2" 12 | ) 13 | 14 | func random(min, max float64) float64 { 15 | return rand.Float64()*(max-min) + min 16 | } 17 | 18 | func main() { 19 | numValues := 1024 20 | numSeries := 100 21 | series := make([]chart.Series, numSeries) 22 | 23 | for i := 0; i < numSeries; i++ { 24 | xValues := make([]time.Time, numValues) 25 | yValues := make([]float64, numValues) 26 | 27 | for j := 0; j < numValues; j++ { 28 | xValues[j] = time.Now().AddDate(0, 0, (numValues-j)*-1) 29 | yValues[j] = random(float64(-500), float64(500)) 30 | } 31 | 32 | series[i] = chart.TimeSeries{ 33 | Name: fmt.Sprintf("aaa.bbb.hostname-%v.ccc.ddd.eee.fff.ggg.hhh.iii.jjj.kkk.lll.mmm.nnn.value", i), 34 | XValues: xValues, 35 | YValues: yValues, 36 | } 37 | } 38 | 39 | graph := chart.Chart{ 40 | XAxis: chart.XAxis{ 41 | Name: "Time", 42 | }, 43 | YAxis: chart.YAxis{ 44 | Name: "Value", 45 | }, 46 | Series: series, 47 | } 48 | 49 | f, _ := os.Create("output.png") 50 | defer f.Close() 51 | graph.Render(chart.PNG, f) 52 | } 53 | -------------------------------------------------------------------------------- /examples/benchmark_line_charts/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/benchmark_line_charts/output.png -------------------------------------------------------------------------------- /examples/css_classes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | // Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example 12 | 13 | func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) { 14 | res.Write([]byte( 15 | "" + 16 | "" + 17 | "" + 18 | "")) 19 | 20 | pie := chart.PieChart{ 21 | // Notes: * Setting ClassName will cause all other inline styles to be dropped! 22 | // * The following type classes may be added additionally: stroke, fill, text 23 | Background: chart.Style{ClassName: "background"}, 24 | Canvas: chart.Style{ 25 | ClassName: "canvas", 26 | }, 27 | Width: 512, 28 | Height: 512, 29 | Values: []chart.Value{ 30 | {Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}}, 31 | {Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}}, 32 | {Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}}, 33 | }, 34 | } 35 | 36 | err := pie.Render(chart.SVG, res) 37 | if err != nil { 38 | fmt.Printf("Error rendering pie chart: %v\n", err) 39 | } 40 | res.Write([]byte("")) 41 | } 42 | 43 | func css(res http.ResponseWriter, req *http.Request) { 44 | res.Header().Set("Content-Type", "text/css") 45 | res.Write([]byte("svg .background { fill: white; }" + 46 | "svg .canvas { fill: white; }" + 47 | "svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" + 48 | "svg .green.fill.stroke { fill: green; stroke: lightgreen; }" + 49 | "svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" + 50 | "svg .blue.text { fill: white; }" + 51 | "svg .green.text { fill: white; }" + 52 | "svg .gray.text { fill: white; }")) 53 | } 54 | 55 | func main() { 56 | http.HandleFunc("/", inlineSVGWithClasses) 57 | http.HandleFunc("/main.css", css) 58 | log.Fatal(http.ListenAndServe(":8080", nil)) 59 | } 60 | -------------------------------------------------------------------------------- /examples/custom_formatters/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/wcharczuk/go-chart/v2" 10 | ) 11 | 12 | func main() { 13 | /* 14 | In this example we use a custom `ValueFormatter` for the y axis, letting us specify how to format text of the y-axis ticks. 15 | You can also do this for the x-axis, or the secondary y-axis. 16 | This example also shows what the chart looks like with the x-axis left off or not shown. 17 | */ 18 | 19 | graph := chart.Chart{ 20 | YAxis: chart.YAxis{ 21 | ValueFormatter: func(v interface{}) string { 22 | if vf, isFloat := v.(float64); isFloat { 23 | return fmt.Sprintf("%0.6f", vf) 24 | } 25 | return "" 26 | }, 27 | }, 28 | Series: []chart.Series{ 29 | chart.ContinuousSeries{ 30 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 31 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 32 | }, 33 | }, 34 | } 35 | f, _ := os.Create("output.png") 36 | defer f.Close() 37 | graph.Render(chart.PNG, f) 38 | } 39 | -------------------------------------------------------------------------------- /examples/custom_formatters/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/custom_formatters/output.png -------------------------------------------------------------------------------- /examples/custom_padding/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | "github.com/wcharczuk/go-chart/v2/drawing" 10 | ) 11 | 12 | func main() { 13 | graph := chart.Chart{ 14 | Background: chart.Style{ 15 | Padding: chart.Box{ 16 | Top: 50, 17 | Left: 25, 18 | Right: 25, 19 | Bottom: 10, 20 | }, 21 | FillColor: drawing.ColorFromHex("efefef"), 22 | }, 23 | Series: []chart.Series{ 24 | chart.ContinuousSeries{ 25 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), 26 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(100).WithMax(512)}.Values(), 27 | }, 28 | }, 29 | } 30 | 31 | f, _ := os.Create("output.png") 32 | defer f.Close() 33 | graph.Render(chart.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /examples/custom_padding/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/custom_padding/output.png -------------------------------------------------------------------------------- /examples/custom_ranges/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we set a custom range for the y-axis, overriding the automatic range generation. 14 | Note: the chart will still generate the ticks automatically based on the custom range, so the intervals may be a bit weird. 15 | */ 16 | 17 | graph := chart.Chart{ 18 | YAxis: chart.YAxis{ 19 | Range: &chart.ContinuousRange{ 20 | Min: 0.0, 21 | Max: 10.0, 22 | }, 23 | }, 24 | Series: []chart.Series{ 25 | chart.ContinuousSeries{ 26 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 27 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 28 | }, 29 | }, 30 | } 31 | f, _ := os.Create("output.png") 32 | defer f.Close() 33 | graph.Render(chart.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /examples/custom_ranges/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/custom_ranges/output.png -------------------------------------------------------------------------------- /examples/custom_styles/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | "github.com/wcharczuk/go-chart/v2/drawing" 10 | ) 11 | 12 | func main() { 13 | /* 14 | In this example we set some custom colors for the series and the chart background and canvas. 15 | */ 16 | graph := chart.Chart{ 17 | Background: chart.Style{ 18 | FillColor: drawing.ColorBlue, 19 | }, 20 | Canvas: chart.Style{ 21 | FillColor: drawing.ColorFromHex("efefef"), 22 | }, 23 | Series: []chart.Series{ 24 | chart.ContinuousSeries{ 25 | Style: chart.Style{ 26 | StrokeColor: drawing.ColorRed, // will supercede defaults 27 | FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults 28 | }, 29 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 30 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 31 | }, 32 | }, 33 | } 34 | 35 | f, _ := os.Create("output.png") 36 | defer f.Close() 37 | graph.Render(chart.PNG, f) 38 | } 39 | -------------------------------------------------------------------------------- /examples/custom_styles/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/custom_styles/output.png -------------------------------------------------------------------------------- /examples/custom_stylesheets/inlineOutput.svg: -------------------------------------------------------------------------------- 1 | \nBlueGreenGray -------------------------------------------------------------------------------- /examples/custom_stylesheets/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | const style = "svg .background { fill: white; }" + 12 | "svg .canvas { fill: white; }" + 13 | "svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" + 14 | "svg .green.fill.stroke { fill: green; stroke: lightgreen; }" + 15 | "svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" + 16 | "svg .blue.text { fill: white; }" + 17 | "svg .green.text { fill: white; }" + 18 | "svg .gray.text { fill: white; }" 19 | 20 | func svgWithCustomInlineCSS(res http.ResponseWriter, _ *http.Request) { 21 | res.Header().Set("Content-Type", chart.ContentTypeSVG) 22 | 23 | // Render the CSS with custom css 24 | err := pieChart().Render(chart.SVGWithCSS(style, ""), res) 25 | if err != nil { 26 | fmt.Printf("Error rendering pie chart: %v\n", err) 27 | } 28 | } 29 | 30 | func svgWithCustomInlineCSSNonce(res http.ResponseWriter, _ *http.Request) { 31 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src 32 | // This should be randomly generated on every request! 33 | const nonce = "RAND0MBASE64" 34 | 35 | res.Header().Set("Content-Security-Policy", fmt.Sprintf("style-src 'nonce-%s'", nonce)) 36 | res.Header().Set("Content-Type", chart.ContentTypeSVG) 37 | 38 | // Render the CSS with custom css and a nonce. 39 | // Try changing the nonce to a different string - your browser should block the CSS. 40 | err := pieChart().Render(chart.SVGWithCSS(style, nonce), res) 41 | if err != nil { 42 | fmt.Printf("Error rendering pie chart: %v\n", err) 43 | } 44 | } 45 | 46 | func svgWithCustomExternalCSS(res http.ResponseWriter, _ *http.Request) { 47 | // Add external CSS 48 | res.Write([]byte( 49 | `` + 50 | `` + 51 | ``)) 52 | 53 | res.Header().Set("Content-Type", chart.ContentTypeSVG) 54 | err := pieChart().Render(chart.SVG, res) 55 | if err != nil { 56 | fmt.Printf("Error rendering pie chart: %v\n", err) 57 | } 58 | } 59 | 60 | func pieChart() chart.PieChart { 61 | return chart.PieChart{ 62 | // Note that setting ClassName will cause all other inline styles to be dropped! 63 | Background: chart.Style{ClassName: "background"}, 64 | Canvas: chart.Style{ 65 | ClassName: "canvas", 66 | }, 67 | Width: 512, 68 | Height: 512, 69 | Values: []chart.Value{ 70 | {Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}}, 71 | {Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}}, 72 | {Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}}, 73 | }, 74 | } 75 | } 76 | 77 | func css(res http.ResponseWriter, req *http.Request) { 78 | res.Header().Set("Content-Type", "text/css") 79 | res.Write([]byte(style)) 80 | } 81 | 82 | func main() { 83 | http.HandleFunc("/", svgWithCustomInlineCSS) 84 | http.HandleFunc("/nonce", svgWithCustomInlineCSSNonce) 85 | http.HandleFunc("/external", svgWithCustomExternalCSS) 86 | http.HandleFunc("/main.css", css) 87 | log.Fatal(http.ListenAndServe(":8080", nil)) 88 | } 89 | -------------------------------------------------------------------------------- /examples/custom_ticks/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we set a custom set of ticks to use for the y-axis. It can be (almost) whatever you want, including some custom labels for ticks. 14 | Custom ticks will supercede a custom range, which will supercede automatic generation based on series values. 15 | */ 16 | 17 | graph := chart.Chart{ 18 | YAxis: chart.YAxis{ 19 | Range: &chart.ContinuousRange{ 20 | Min: 0.0, 21 | Max: 4.0, 22 | }, 23 | Ticks: []chart.Tick{ 24 | {Value: 0.0, Label: "0.00"}, 25 | {Value: 2.0, Label: "2.00"}, 26 | {Value: 4.0, Label: "4.00"}, 27 | {Value: 6.0, Label: "6.00"}, 28 | {Value: 8.0, Label: "Eight"}, 29 | {Value: 10.0, Label: "Ten"}, 30 | }, 31 | }, 32 | Series: []chart.Series{ 33 | chart.ContinuousSeries{ 34 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 35 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 36 | }, 37 | }, 38 | } 39 | f, _ := os.Create("output.png") 40 | defer f.Close() 41 | graph.Render(chart.PNG, f) 42 | } 43 | -------------------------------------------------------------------------------- /examples/custom_ticks/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/custom_ticks/output.png -------------------------------------------------------------------------------- /examples/descending/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 15 | In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, 16 | the canvas "box" is adjusted to fit the space the axes occupy so as not to clip. 17 | Additionally, it shows how you can use the "Descending" property of continuous ranges to change the ordering of 18 | how values (including ticks) are drawn. 19 | */ 20 | 21 | graph := chart.Chart{ 22 | Height: 500, 23 | Width: 500, 24 | XAxis: chart.XAxis{ 25 | /*Range: &chart.ContinuousRange{ 26 | Descending: true, 27 | },*/ 28 | }, 29 | YAxis: chart.YAxis{ 30 | Range: &chart.ContinuousRange{ 31 | Descending: true, 32 | }, 33 | }, 34 | Series: []chart.Series{ 35 | chart.ContinuousSeries{ 36 | Style: chart.Style{ 37 | StrokeColor: chart.GetDefaultColor(0).WithAlpha(64), 38 | FillColor: chart.GetDefaultColor(0).WithAlpha(64), 39 | }, 40 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 41 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 42 | }, 43 | }, 44 | } 45 | 46 | f, _ := os.Create("output.png") 47 | defer f.Close() 48 | graph.Render(chart.PNG, f) 49 | } 50 | -------------------------------------------------------------------------------- /examples/descending/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/descending/output.png -------------------------------------------------------------------------------- /examples/donut_chart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | pie := chart.DonutChart{ 13 | Width: 512, 14 | Height: 512, 15 | Values: []chart.Value{ 16 | {Value: 5, Label: "Blue"}, 17 | {Value: 5, Label: "Green"}, 18 | {Value: 4, Label: "Gray"}, 19 | {Value: 4, Label: "Orange"}, 20 | {Value: 3, Label: "Deep Blue"}, 21 | {Value: 3, Label: "test"}, 22 | }, 23 | } 24 | 25 | f, _ := os.Create("output.png") 26 | defer f.Close() 27 | pie.Render(chart.PNG, f) 28 | } 29 | -------------------------------------------------------------------------------- /examples/donut_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/donut_chart/output.png -------------------------------------------------------------------------------- /examples/donut_chart/reg.svg: -------------------------------------------------------------------------------- 1 | \nBlueTwoOne -------------------------------------------------------------------------------- /examples/horizontal_stacked_bar/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/horizontal_stacked_bar/output.png -------------------------------------------------------------------------------- /examples/image_writer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/wcharczuk/go-chart/v2" 8 | ) 9 | 10 | func main() { 11 | graph := chart.Chart{ 12 | Series: []chart.Series{ 13 | chart.ContinuousSeries{ 14 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 15 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 16 | }, 17 | }, 18 | } 19 | collector := &chart.ImageWriter{} 20 | graph.Render(chart.PNG, collector) 21 | 22 | image, err := collector.Image() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | fmt.Printf("Final Image: %dx%d\n", image.Bounds().Size().X, image.Bounds().Size().Y) 27 | } 28 | -------------------------------------------------------------------------------- /examples/legend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | chart "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | In this example we add a `Renderable` or a custom component to the `Elements` array. 15 | In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series. 16 | If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend. 17 | */ 18 | 19 | graph := chart.Chart{ 20 | Background: chart.Style{ 21 | Padding: chart.Box{ 22 | Top: 20, 23 | Left: 20, 24 | }, 25 | }, 26 | Series: []chart.Series{ 27 | chart.ContinuousSeries{ 28 | Name: "A test series", 29 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 30 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 31 | }, 32 | }, 33 | } 34 | 35 | //note we have to do this as a separate step because we need a reference to graph 36 | graph.Elements = []chart.Renderable{ 37 | chart.Legend(&graph), 38 | } 39 | 40 | f, _ := os.Create("output.png") 41 | defer f.Close() 42 | graph.Render(chart.PNG, f) 43 | } 44 | -------------------------------------------------------------------------------- /examples/legend/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/legend/output.png -------------------------------------------------------------------------------- /examples/legend_left/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | In this example we add a `Renderable` or a custom component to the `Elements` array. 15 | In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series. 16 | If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend. 17 | */ 18 | 19 | graph := chart.Chart{ 20 | Background: chart.Style{ 21 | Padding: chart.Box{ 22 | Top: 20, 23 | Left: 260, 24 | }, 25 | }, 26 | Series: []chart.Series{ 27 | chart.ContinuousSeries{ 28 | Name: "A test series", 29 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 30 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 31 | }, 32 | 33 | chart.ContinuousSeries{ 34 | Name: "Another test series", 35 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 36 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 37 | }, 38 | 39 | chart.ContinuousSeries{ 40 | Name: "Yet Another test series", 41 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 42 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 43 | }, 44 | 45 | chart.ContinuousSeries{ 46 | Name: "Even More series", 47 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 48 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 49 | }, 50 | 51 | chart.ContinuousSeries{ 52 | Name: "Foo Bar", 53 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 54 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 55 | }, 56 | 57 | chart.ContinuousSeries{ 58 | Name: "Bar Baz", 59 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 60 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 61 | }, 62 | 63 | chart.ContinuousSeries{ 64 | Name: "Moo Bar", 65 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 66 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 67 | }, 68 | 69 | chart.ContinuousSeries{ 70 | Name: "Zoo Bar Baz", 71 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 72 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 73 | }, 74 | 75 | chart.ContinuousSeries{ 76 | Name: "Fast and the Furious", 77 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 78 | YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, 79 | }, 80 | 81 | chart.ContinuousSeries{ 82 | Name: "2 Fast 2 Furious", 83 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 84 | YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, 85 | }, 86 | 87 | chart.ContinuousSeries{ 88 | Name: "They only get more fast and more furious", 89 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 90 | YValues: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, 91 | }, 92 | }, 93 | } 94 | 95 | //note we have to do this as a separate step because we need a reference to graph 96 | graph.Elements = []chart.Renderable{ 97 | chart.LegendLeft(&graph), 98 | } 99 | 100 | f, _ := os.Create("output.png") 101 | defer f.Close() 102 | graph.Render(chart.PNG, f) 103 | } 104 | -------------------------------------------------------------------------------- /examples/legend_left/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/legend_left/output.png -------------------------------------------------------------------------------- /examples/linear_regression/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | chart "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument. 15 | InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. 16 | */ 17 | 18 | mainSeries := chart.ContinuousSeries{ 19 | Name: "A test series", 20 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. 21 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements. 22 | } 23 | 24 | // note we create a LinearRegressionSeries series by assignin the inner series. 25 | // we need to use a reference because `.Render()` needs to modify state within the series. 26 | linRegSeries := &chart.LinearRegressionSeries{ 27 | InnerSeries: mainSeries, 28 | } // we can optionally set the `WindowSize` property which alters how the moving average is calculated. 29 | 30 | graph := chart.Chart{ 31 | Series: []chart.Series{ 32 | mainSeries, 33 | linRegSeries, 34 | }, 35 | } 36 | 37 | f, _ := os.Create("output.png") 38 | defer f.Close() 39 | graph.Render(chart.PNG, f) 40 | } 41 | -------------------------------------------------------------------------------- /examples/linear_regression/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/linear_regression/output.png -------------------------------------------------------------------------------- /examples/logarithmic_axes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | In this example we set the primary YAxis to have logarithmic range. 15 | */ 16 | 17 | graph := chart.Chart{ 18 | Background: chart.Style{ 19 | Padding: chart.Box{ 20 | Top: 20, 21 | Left: 20, 22 | }, 23 | }, 24 | Series: []chart.Series{ 25 | chart.ContinuousSeries{ 26 | Name: "A test series", 27 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 28 | YValues: []float64{1, 10, 100, 1000, 10000}, 29 | }, 30 | }, 31 | YAxis: chart.YAxis{ 32 | Style: chart.Shown(), 33 | NameStyle: chart.Shown(), 34 | Range: &chart.LogarithmicRange{}, 35 | }, 36 | } 37 | 38 | f, _ := os.Create("output.png") 39 | defer f.Close() 40 | graph.Render(chart.PNG, f) 41 | } 42 | -------------------------------------------------------------------------------- /examples/logarithmic_axes/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/logarithmic_axes/output.png -------------------------------------------------------------------------------- /examples/min_max/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | mainSeries := chart.ContinuousSeries{ 13 | Name: "A test series", 14 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), 15 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(50).WithMax(150)}.Values(), 16 | } 17 | 18 | minSeries := &chart.MinSeries{ 19 | Style: chart.Style{ 20 | StrokeColor: chart.ColorAlternateGray, 21 | StrokeDashArray: []float64{5.0, 5.0}, 22 | }, 23 | InnerSeries: mainSeries, 24 | } 25 | 26 | maxSeries := &chart.MaxSeries{ 27 | Style: chart.Style{ 28 | StrokeColor: chart.ColorAlternateGray, 29 | StrokeDashArray: []float64{5.0, 5.0}, 30 | }, 31 | InnerSeries: mainSeries, 32 | } 33 | 34 | graph := chart.Chart{ 35 | Width: 1920, 36 | Height: 1080, 37 | YAxis: chart.YAxis{ 38 | Name: "Random Values", 39 | Range: &chart.ContinuousRange{ 40 | Min: 25, 41 | Max: 175, 42 | }, 43 | }, 44 | XAxis: chart.XAxis{ 45 | Name: "Random Other Values", 46 | }, 47 | Series: []chart.Series{ 48 | mainSeries, 49 | minSeries, 50 | maxSeries, 51 | chart.LastValueAnnotationSeries(minSeries), 52 | chart.LastValueAnnotationSeries(maxSeries), 53 | }, 54 | } 55 | 56 | graph.Elements = []chart.Renderable{chart.Legend(&graph)} 57 | 58 | f, _ := os.Create("output.png") 59 | defer f.Close() 60 | graph.Render(chart.PNG, f) 61 | } 62 | -------------------------------------------------------------------------------- /examples/min_max/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/min_max/output.png -------------------------------------------------------------------------------- /examples/pie_chart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | pie := chart.PieChart{ 13 | Width: 512, 14 | Height: 512, 15 | Values: []chart.Value{ 16 | {Value: 5, Label: "Blue"}, 17 | {Value: 5, Label: "Green"}, 18 | {Value: 4, Label: "Gray"}, 19 | {Value: 4, Label: "Orange"}, 20 | {Value: 3, Label: "Deep Blue"}, 21 | {Value: 3, Label: "??"}, 22 | {Value: 1, Label: "!!"}, 23 | }, 24 | } 25 | 26 | f, _ := os.Create("output.png") 27 | defer f.Close() 28 | pie.Render(chart.PNG, f) 29 | } 30 | -------------------------------------------------------------------------------- /examples/pie_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/pie_chart/output.png -------------------------------------------------------------------------------- /examples/pie_chart/reg.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | -------------------------------------------------------------------------------- /examples/poly_regression/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | chart "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | /* 14 | In this example we add a new type of series, a `PolynomialRegressionSeries` that takes another series as a required argument. 15 | InnerSeries only needs to implement `ValuesProvider`, so really you could chain `PolynomialRegressionSeries` together if you wanted. 16 | */ 17 | 18 | mainSeries := chart.ContinuousSeries{ 19 | Name: "A test series", 20 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. 21 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements. 22 | } 23 | 24 | polyRegSeries := &chart.PolynomialRegressionSeries{ 25 | Degree: 3, 26 | InnerSeries: mainSeries, 27 | } 28 | 29 | graph := chart.Chart{ 30 | Series: []chart.Series{ 31 | mainSeries, 32 | polyRegSeries, 33 | }, 34 | } 35 | 36 | f, _ := os.Create("output.png") 37 | defer f.Close() 38 | graph.Render(chart.PNG, f) 39 | } 40 | -------------------------------------------------------------------------------- /examples/poly_regression/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/poly_regression/output.png -------------------------------------------------------------------------------- /examples/request_timings/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/wcharczuk/go-chart/v2" 13 | ) 14 | 15 | func main() { 16 | log := chart.NewLogger() 17 | drawChart(log) 18 | } 19 | 20 | func parseInt(str string) int { 21 | v, _ := strconv.Atoi(str) 22 | return v 23 | } 24 | 25 | func parseFloat64(str string) float64 { 26 | v, _ := strconv.ParseFloat(str, 64) 27 | return v 28 | } 29 | 30 | func readData() ([]time.Time, []float64) { 31 | var xvalues []time.Time 32 | var yvalues []float64 33 | err := chart.ReadLines("requests.csv", func(line string) error { 34 | parts := chart.SplitCSV(line) 35 | year := parseInt(parts[0]) 36 | month := parseInt(parts[1]) 37 | day := parseInt(parts[2]) 38 | hour := parseInt(parts[3]) 39 | elapsedMillis := parseFloat64(parts[4]) 40 | xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC)) 41 | yvalues = append(yvalues, elapsedMillis) 42 | return nil 43 | }) 44 | if err != nil { 45 | fmt.Println(err.Error()) 46 | } 47 | return xvalues, yvalues 48 | } 49 | 50 | func releases() []chart.GridLine { 51 | return []chart.GridLine{ 52 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))}, 53 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))}, 54 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 2, 15, 30, 0, 0, time.UTC))}, 55 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))}, 56 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))}, 57 | {Value: chart.TimeToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))}, 58 | } 59 | } 60 | 61 | func drawChart(log chart.Logger) http.HandlerFunc { 62 | return func(res http.ResponseWriter, req *http.Request) { 63 | xvalues, yvalues := readData() 64 | mainSeries := chart.TimeSeries{ 65 | Name: "Prod Request Timings", 66 | Style: chart.Style{ 67 | StrokeColor: chart.ColorBlue, 68 | FillColor: chart.ColorBlue.WithAlpha(100), 69 | }, 70 | XValues: xvalues, 71 | YValues: yvalues, 72 | } 73 | 74 | linreg := &chart.LinearRegressionSeries{ 75 | Name: "Linear Regression", 76 | Style: chart.Style{ 77 | StrokeColor: chart.ColorAlternateBlue, 78 | StrokeDashArray: []float64{5.0, 5.0}, 79 | }, 80 | InnerSeries: mainSeries, 81 | } 82 | 83 | sma := &chart.SMASeries{ 84 | Name: "SMA", 85 | Style: chart.Style{ 86 | StrokeColor: chart.ColorRed, 87 | StrokeDashArray: []float64{5.0, 5.0}, 88 | }, 89 | InnerSeries: mainSeries, 90 | } 91 | 92 | graph := chart.Chart{ 93 | Log: log, 94 | Width: 1280, 95 | Height: 720, 96 | Background: chart.Style{ 97 | Padding: chart.Box{ 98 | Top: 50, 99 | }, 100 | }, 101 | YAxis: chart.YAxis{ 102 | Name: "Elapsed Millis", 103 | TickStyle: chart.Style{ 104 | TextRotationDegrees: 45.0, 105 | }, 106 | ValueFormatter: func(v interface{}) string { 107 | return fmt.Sprintf("%d ms", int(v.(float64))) 108 | }, 109 | }, 110 | XAxis: chart.XAxis{ 111 | ValueFormatter: chart.TimeHourValueFormatter, 112 | GridMajorStyle: chart.Style{ 113 | StrokeColor: chart.ColorAlternateGray, 114 | StrokeWidth: 1.0, 115 | }, 116 | GridLines: releases(), 117 | }, 118 | Series: []chart.Series{ 119 | mainSeries, 120 | linreg, 121 | chart.LastValueAnnotationSeries(linreg), 122 | sma, 123 | chart.LastValueAnnotationSeries(sma), 124 | }, 125 | } 126 | 127 | graph.Elements = []chart.Renderable{chart.LegendThin(&graph)} 128 | 129 | f, _ := os.Create("output.png") 130 | defer f.Close() 131 | graph.Render(chart.PNG, f) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/request_timings/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/request_timings/output.png -------------------------------------------------------------------------------- /examples/rerender/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "github.com/wcharczuk/go-chart/v2" 10 | ) 11 | 12 | var lock sync.Mutex 13 | var graph *chart.Chart 14 | var ts *chart.TimeSeries 15 | 16 | func addData(t time.Time, e time.Duration) { 17 | lock.Lock() 18 | ts.XValues = append(ts.XValues, t) 19 | ts.YValues = append(ts.YValues, chart.TimeMillis(e)) 20 | lock.Unlock() 21 | } 22 | 23 | func drawChart(res http.ResponseWriter, req *http.Request) { 24 | start := time.Now() 25 | defer func() { 26 | addData(start, time.Since(start)) 27 | }() 28 | if len(ts.XValues) == 0 { 29 | http.Error(res, "no data (yet)", http.StatusBadRequest) 30 | return 31 | } 32 | res.Header().Set("Content-Type", "image/png") 33 | if err := graph.Render(chart.PNG, res); err != nil { 34 | log.Printf("%v", err) 35 | } 36 | } 37 | 38 | func main() { 39 | ts = &chart.TimeSeries{ 40 | XValues: []time.Time{}, 41 | YValues: []float64{}, 42 | } 43 | graph = &chart.Chart{ 44 | Series: []chart.Series{ts}, 45 | } 46 | http.HandleFunc("/", drawChart) 47 | log.Fatal(http.ListenAndServe(":8080", nil)) 48 | } 49 | -------------------------------------------------------------------------------- /examples/scatter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | _ "net/http/pprof" 8 | 9 | "github.com/wcharczuk/go-chart/v2" 10 | "github.com/wcharczuk/go-chart/v2/drawing" 11 | ) 12 | 13 | func drawChart(res http.ResponseWriter, req *http.Request) { 14 | 15 | viridisByY := func(xr, yr chart.Range, index int, x, y float64) drawing.Color { 16 | return chart.Viridis(y, yr.GetMin(), yr.GetMax()) 17 | } 18 | 19 | graph := chart.Chart{ 20 | Series: []chart.Series{ 21 | chart.ContinuousSeries{ 22 | Style: chart.Style{ 23 | StrokeWidth: chart.Disabled, 24 | DotWidth: 5, 25 | DotColorProvider: viridisByY, 26 | }, 27 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(127)}.Values(), 28 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(128).WithMin(0).WithMax(1024)}.Values(), 29 | }, 30 | }, 31 | } 32 | 33 | res.Header().Set("Content-Type", chart.ContentTypePNG) 34 | err := graph.Render(chart.PNG, res) 35 | if err != nil { 36 | log.Println(err.Error()) 37 | } 38 | } 39 | 40 | func unit(res http.ResponseWriter, req *http.Request) { 41 | graph := chart.Chart{ 42 | Height: 50, 43 | Width: 50, 44 | Canvas: chart.Style{ 45 | Padding: chart.BoxZero, 46 | }, 47 | Background: chart.Style{ 48 | Padding: chart.BoxZero, 49 | }, 50 | Series: []chart.Series{ 51 | chart.ContinuousSeries{ 52 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), 53 | YValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), 54 | }, 55 | }, 56 | } 57 | 58 | res.Header().Set("Content-Type", chart.ContentTypePNG) 59 | err := graph.Render(chart.PNG, res) 60 | if err != nil { 61 | log.Println(err.Error()) 62 | } 63 | } 64 | 65 | func main() { 66 | http.HandleFunc("/", drawChart) 67 | http.HandleFunc("/unit", unit) 68 | log.Fatal(http.ListenAndServe(":8080", nil)) 69 | } 70 | -------------------------------------------------------------------------------- /examples/scatter/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/scatter/output.png -------------------------------------------------------------------------------- /examples/simple_moving_average/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | ) 10 | 11 | func main() { 12 | mainSeries := chart.ContinuousSeries{ 13 | Name: "A test series", 14 | XValues: chart.Seq{Sequence: chart.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. 15 | YValues: chart.Seq{Sequence: chart.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements. 16 | } 17 | 18 | // note we create a SimpleMovingAverage series by assignin the inner series. 19 | // we need to use a reference because `.Render()` needs to modify state within the series. 20 | smaSeries := &chart.SMASeries{ 21 | InnerSeries: mainSeries, 22 | } // we can optionally set the `WindowSize` property which alters how the moving average is calculated. 23 | 24 | graph := chart.Chart{ 25 | Series: []chart.Series{ 26 | mainSeries, 27 | smaSeries, 28 | }, 29 | } 30 | 31 | f, _ := os.Create("output.png") 32 | defer f.Close() 33 | graph.Render(chart.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /examples/simple_moving_average/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/simple_moving_average/output.png -------------------------------------------------------------------------------- /examples/stacked_bar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/wcharczuk/go-chart/v2" 7 | ) 8 | 9 | func main() { 10 | sbc := chart.StackedBarChart{ 11 | Title: "Test Stacked Bar Chart", 12 | Background: chart.Style{ 13 | Padding: chart.Box{ 14 | Top: 40, 15 | }, 16 | }, 17 | Height: 512, 18 | Bars: []chart.StackedBar{ 19 | { 20 | Name: "This is a very long string to test word break wrapping.", 21 | Values: []chart.Value{ 22 | {Value: 5, Label: "Blue"}, 23 | {Value: 5, Label: "Green"}, 24 | {Value: 4, Label: "Gray"}, 25 | {Value: 3, Label: "Orange"}, 26 | {Value: 3, Label: "Test"}, 27 | {Value: 2, Label: "??"}, 28 | {Value: 1, Label: "!!"}, 29 | }, 30 | }, 31 | { 32 | Name: "Test", 33 | Values: []chart.Value{ 34 | {Value: 10, Label: "Blue"}, 35 | {Value: 5, Label: "Green"}, 36 | {Value: 1, Label: "Gray"}, 37 | }, 38 | }, 39 | { 40 | Name: "Test 2", 41 | Values: []chart.Value{ 42 | {Value: 10, Label: "Blue"}, 43 | {Value: 6, Label: "Green"}, 44 | {Value: 4, Label: "Gray"}, 45 | }, 46 | }, 47 | }, 48 | } 49 | 50 | f, _ := os.Create("output.png") 51 | defer f.Close() 52 | sbc.Render(chart.PNG, f) 53 | } 54 | -------------------------------------------------------------------------------- /examples/stacked_bar/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/stacked_bar/output.png -------------------------------------------------------------------------------- /examples/stacked_bar_labels/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/stacked_bar_labels/output.png -------------------------------------------------------------------------------- /examples/stock_analysis/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/stock_analysis/output.png -------------------------------------------------------------------------------- /examples/text_rotation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/wcharczuk/go-chart/v2" 9 | "github.com/wcharczuk/go-chart/v2/drawing" 10 | ) 11 | 12 | func main() { 13 | f, _ := chart.GetDefaultFont() 14 | r, _ := chart.PNG(1024, 1024) 15 | 16 | chart.Draw.Text(r, "Test", 64, 64, chart.Style{ 17 | FontColor: drawing.ColorBlack, 18 | FontSize: 18, 19 | Font: f, 20 | }) 21 | 22 | chart.Draw.Text(r, "Test", 64, 64, chart.Style{ 23 | FontColor: drawing.ColorBlack, 24 | FontSize: 18, 25 | Font: f, 26 | TextRotationDegrees: 45.0, 27 | }) 28 | 29 | tb := chart.Draw.MeasureText(r, "Test", chart.Style{ 30 | FontColor: drawing.ColorBlack, 31 | FontSize: 18, 32 | Font: f, 33 | }).Shift(64, 64) 34 | 35 | tbc := tb.Corners().Rotate(45) 36 | 37 | chart.Draw.BoxCorners(r, tbc, chart.Style{ 38 | StrokeColor: drawing.ColorRed, 39 | StrokeWidth: 2, 40 | }) 41 | 42 | tbcb := tbc.Box() 43 | chart.Draw.Box(r, tbcb, chart.Style{ 44 | StrokeColor: drawing.ColorBlue, 45 | StrokeWidth: 2, 46 | }) 47 | 48 | file, _ := os.Create("output.png") 49 | defer file.Close() 50 | r.Save(file) 51 | } 52 | -------------------------------------------------------------------------------- /examples/text_rotation/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/text_rotation/output.png -------------------------------------------------------------------------------- /examples/timeseries/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | chart "github.com/wcharczuk/go-chart/v2" 8 | ) 9 | 10 | func drawChart(res http.ResponseWriter, req *http.Request) { 11 | /* 12 | This is an example of using the `TimeSeries` to automatically coerce time.Time values into a continuous xrange. 13 | Note: chart.TimeSeries implements `ValueFormatterProvider` and as a result gives the XAxis the appropriate formatter to use for the ticks. 14 | */ 15 | graph := chart.Chart{ 16 | Series: []chart.Series{ 17 | chart.TimeSeries{ 18 | XValues: []time.Time{ 19 | time.Now().AddDate(0, 0, -10), 20 | time.Now().AddDate(0, 0, -9), 21 | time.Now().AddDate(0, 0, -8), 22 | time.Now().AddDate(0, 0, -7), 23 | time.Now().AddDate(0, 0, -6), 24 | time.Now().AddDate(0, 0, -5), 25 | time.Now().AddDate(0, 0, -4), 26 | time.Now().AddDate(0, 0, -3), 27 | time.Now().AddDate(0, 0, -2), 28 | time.Now().AddDate(0, 0, -1), 29 | time.Now(), 30 | }, 31 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0}, 32 | }, 33 | }, 34 | } 35 | 36 | res.Header().Set("Content-Type", "image/png") 37 | graph.Render(chart.PNG, res) 38 | } 39 | 40 | func drawCustomChart(res http.ResponseWriter, req *http.Request) { 41 | /* 42 | This is basically the other timeseries example, except we switch to hour intervals and specify a different formatter from default for the xaxis tick labels. 43 | */ 44 | graph := chart.Chart{ 45 | XAxis: chart.XAxis{ 46 | ValueFormatter: chart.TimeHourValueFormatter, 47 | }, 48 | Series: []chart.Series{ 49 | chart.TimeSeries{ 50 | XValues: []time.Time{ 51 | time.Now().Add(-10 * time.Hour), 52 | time.Now().Add(-9 * time.Hour), 53 | time.Now().Add(-8 * time.Hour), 54 | time.Now().Add(-7 * time.Hour), 55 | time.Now().Add(-6 * time.Hour), 56 | time.Now().Add(-5 * time.Hour), 57 | time.Now().Add(-4 * time.Hour), 58 | time.Now().Add(-3 * time.Hour), 59 | time.Now().Add(-2 * time.Hour), 60 | time.Now().Add(-1 * time.Hour), 61 | time.Now(), 62 | }, 63 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0}, 64 | }, 65 | }, 66 | } 67 | 68 | res.Header().Set("Content-Type", "image/png") 69 | graph.Render(chart.PNG, res) 70 | } 71 | 72 | func main() { 73 | http.HandleFunc("/", drawChart) 74 | http.HandleFunc("/favicon.ico", func(res http.ResponseWriter, req *http.Request) { 75 | res.Write([]byte{}) 76 | }) 77 | http.HandleFunc("/custom", drawCustomChart) 78 | http.ListenAndServe(":8080", nil) 79 | } 80 | -------------------------------------------------------------------------------- /examples/timeseries/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/timeseries/output.png -------------------------------------------------------------------------------- /examples/twoaxis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | 9 | "github.com/wcharczuk/go-chart/v2" 10 | ) 11 | 12 | func main() { 13 | 14 | /* 15 | In this example we add a second series, and assign it to the secondary y axis, giving that series it's own range. 16 | 17 | We also enable all of the axes by setting the `Show` propery of their respective styles to `true`. 18 | */ 19 | 20 | graph := chart.Chart{ 21 | XAxis: chart.XAxis{ 22 | TickPosition: chart.TickPositionBetweenTicks, 23 | ValueFormatter: func(v interface{}) string { 24 | typed := v.(float64) 25 | typedDate := chart.TimeFromFloat64(typed) 26 | return fmt.Sprintf("%d-%d\n%d", typedDate.Month(), typedDate.Day(), typedDate.Year()) 27 | }, 28 | }, 29 | Series: []chart.Series{ 30 | chart.ContinuousSeries{ 31 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 32 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 33 | }, 34 | chart.ContinuousSeries{ 35 | YAxis: chart.YAxisSecondary, 36 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 37 | YValues: []float64{50.0, 40.0, 30.0, 20.0, 10.0}, 38 | }, 39 | }, 40 | } 41 | 42 | f, _ := os.Create("output.png") 43 | defer f.Close() 44 | graph.Render(chart.PNG, f) 45 | } 46 | -------------------------------------------------------------------------------- /examples/twoaxis/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/twoaxis/output.png -------------------------------------------------------------------------------- /examples/twopoint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "bytes" 7 | "log" 8 | "os" 9 | 10 | "github.com/wcharczuk/go-chart/v2" 11 | ) 12 | 13 | func main() { 14 | var b float64 15 | b = 1000 16 | 17 | ts1 := chart.ContinuousSeries{ //TimeSeries{ 18 | Name: "Time Series", 19 | XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b}, 20 | YValues: []float64{1.0, 2.0, 30.0, 4.0, 50.0, 6.0, 7.0, 88.0}, 21 | } 22 | 23 | ts2 := chart.ContinuousSeries{ //TimeSeries{ 24 | Style: chart.Style{ 25 | StrokeColor: chart.GetDefaultColor(1), 26 | }, 27 | 28 | XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b}, 29 | YValues: []float64{15.0, 52.0, 30.0, 42.0, 50.0, 26.0, 77.0, 38.0}, 30 | } 31 | 32 | graph := chart.Chart{ 33 | 34 | XAxis: chart.XAxis{ 35 | Name: "The XAxis", 36 | ValueFormatter: chart.TimeMinuteValueFormatter, //TimeHourValueFormatter, 37 | }, 38 | 39 | YAxis: chart.YAxis{ 40 | Name: "The YAxis", 41 | }, 42 | 43 | Series: []chart.Series{ 44 | ts1, 45 | ts2, 46 | }, 47 | } 48 | 49 | buffer := bytes.NewBuffer([]byte{}) 50 | err := graph.Render(chart.PNG, buffer) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | fo, err := os.Create("output.png") 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | if _, err := fo.Write(buffer.Bytes()); err != nil { 61 | panic(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/twopoint/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wcharczuk/go-chart/b46667ea80896feed3f95a0bf6f49e1c4ce02ace/examples/twopoint/output.png -------------------------------------------------------------------------------- /fileutil.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // ReadLines reads a file and calls the handler for each line. 10 | func ReadLines(filePath string, handler func(string) error) error { 11 | f, err := os.Open(filePath) 12 | if err != nil { 13 | return err 14 | } 15 | defer f.Close() 16 | 17 | scanner := bufio.NewScanner(f) 18 | for scanner.Scan() { 19 | line := scanner.Text() 20 | err = handler(line) 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | // ReadChunks reads a file in `chunkSize` pieces, dispatched to the handler. 29 | func ReadChunks(filePath string, chunkSize int, handler func([]byte) error) error { 30 | f, err := os.Open(filePath) 31 | if err != nil { 32 | return err 33 | } 34 | defer f.Close() 35 | 36 | chunk := make([]byte, chunkSize) 37 | for { 38 | readBytes, err := f.Read(chunk) 39 | if err == io.EOF { 40 | break 41 | } 42 | readData := chunk[:readBytes] 43 | err = handler(readData) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /first_value_annotation.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | // FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation. 6 | func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { 7 | var vf ValueFormatter 8 | if len(vfs) > 0 { 9 | vf = vfs[0] 10 | } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { 11 | _, vf = typed.GetValueFormatters() 12 | } else { 13 | vf = FloatValueFormatter 14 | } 15 | 16 | var firstValue Value2 17 | if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped { 18 | firstValue.XValue, firstValue.YValue = typed.GetFirstValues() 19 | firstValue.Label = vf(firstValue.YValue) 20 | } else { 21 | firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0) 22 | firstValue.Label = vf(firstValue.YValue) 23 | } 24 | 25 | var seriesName string 26 | var seriesStyle Style 27 | if typed, isTyped := innerSeries.(Series); isTyped { 28 | seriesName = fmt.Sprintf("%s - First Value", typed.GetName()) 29 | seriesStyle = typed.GetStyle() 30 | } 31 | 32 | return AnnotationSeries{ 33 | Name: seriesName, 34 | Style: seriesStyle, 35 | Annotations: []Value2{firstValue}, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /first_value_annotation_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestFirstValueAnnotation(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | series := ContinuousSeries{ 13 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 14 | YValues: []float64{5.0, 3.0, 3.0, 2.0, 1.0}, 15 | } 16 | 17 | fva := FirstValueAnnotation(series) 18 | testutil.AssertNotEmpty(t, fva.Annotations) 19 | fvaa := fva.Annotations[0] 20 | testutil.AssertEqual(t, 1, fvaa.XValue) 21 | testutil.AssertEqual(t, 5, fvaa.YValue) 22 | } 23 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/golang/freetype/truetype" 7 | "github.com/wcharczuk/go-chart/v2/roboto" 8 | ) 9 | 10 | var ( 11 | _defaultFontLock sync.Mutex 12 | _defaultFont *truetype.Font 13 | ) 14 | 15 | // GetDefaultFont returns the default font (Roboto-Medium). 16 | func GetDefaultFont() (*truetype.Font, error) { 17 | if _defaultFont == nil { 18 | _defaultFontLock.Lock() 19 | defer _defaultFontLock.Unlock() 20 | if _defaultFont == nil { 21 | font, err := truetype.Parse(roboto.Roboto) 22 | if err != nil { 23 | return nil, err 24 | } 25 | _defaultFont = font 26 | } 27 | } 28 | return _defaultFont, nil 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wcharczuk/go-chart/v2 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 7 | golang.org/x/image v0.18.0 8 | ) 9 | -------------------------------------------------------------------------------- /grid_line.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // GridLineProvider is a type that provides grid lines. 4 | type GridLineProvider interface { 5 | GetGridLines(ticks []Tick, isVertical bool, majorStyle, minorStyle Style) []GridLine 6 | } 7 | 8 | // GridLine is a line on a graph canvas. 9 | type GridLine struct { 10 | IsMinor bool 11 | Style Style 12 | Value float64 13 | } 14 | 15 | // Major returns if the gridline is a `major` line. 16 | func (gl GridLine) Major() bool { 17 | return !gl.IsMinor 18 | } 19 | 20 | // Minor returns if the gridline is a `minor` line. 21 | func (gl GridLine) Minor() bool { 22 | return gl.IsMinor 23 | } 24 | 25 | // Render renders the gridline 26 | func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range, isVertical bool, defaults Style) { 27 | r.SetStrokeColor(gl.Style.GetStrokeColor(defaults.GetStrokeColor())) 28 | r.SetStrokeWidth(gl.Style.GetStrokeWidth(defaults.GetStrokeWidth())) 29 | r.SetStrokeDashArray(gl.Style.GetStrokeDashArray(defaults.GetStrokeDashArray())) 30 | 31 | if isVertical { 32 | lineLeft := canvasBox.Left + ra.Translate(gl.Value) 33 | lineBottom := canvasBox.Bottom 34 | lineTop := canvasBox.Top 35 | 36 | r.MoveTo(lineLeft, lineBottom) 37 | r.LineTo(lineLeft, lineTop) 38 | r.Stroke() 39 | } else { 40 | lineLeft := canvasBox.Left 41 | lineRight := canvasBox.Right 42 | lineHeight := canvasBox.Bottom - ra.Translate(gl.Value) 43 | 44 | r.MoveTo(lineLeft, lineHeight) 45 | r.LineTo(lineRight, lineHeight) 46 | r.Stroke() 47 | } 48 | } 49 | 50 | // GenerateGridLines generates grid lines. 51 | func GenerateGridLines(ticks []Tick, majorStyle, minorStyle Style) []GridLine { 52 | var gl []GridLine 53 | isMinor := false 54 | 55 | if len(ticks) < 3 { 56 | return gl 57 | } 58 | 59 | for _, t := range ticks[1 : len(ticks)-1] { 60 | s := majorStyle 61 | if isMinor { 62 | s = minorStyle 63 | } 64 | gl = append(gl, GridLine{ 65 | Style: s, 66 | IsMinor: isMinor, 67 | Value: t.Value, 68 | }) 69 | isMinor = !isMinor 70 | } 71 | return gl 72 | } 73 | -------------------------------------------------------------------------------- /grid_line_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestGenerateGridLines(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | ticks := []Tick{ 13 | {Value: 1.0, Label: "1.0"}, 14 | {Value: 2.0, Label: "2.0"}, 15 | {Value: 3.0, Label: "3.0"}, 16 | {Value: 4.0, Label: "4.0"}, 17 | } 18 | 19 | gl := GenerateGridLines(ticks, Style{}, Style{}) 20 | testutil.AssertLen(t, gl, 2) 21 | 22 | testutil.AssertEqual(t, 2.0, gl[0].Value) 23 | testutil.AssertEqual(t, 3.0, gl[1].Value) 24 | } 25 | -------------------------------------------------------------------------------- /histogram_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | // HistogramSeries is a special type of series that draws as a histogram. 6 | // Some peculiarities; it will always be lower bounded at 0 (at the very least). 7 | // This may alter ranges a bit and generally you want to put a histogram series on it's own y-axis. 8 | type HistogramSeries struct { 9 | Name string 10 | Style Style 11 | YAxis YAxisType 12 | InnerSeries ValuesProvider 13 | } 14 | 15 | // GetName implements Series.GetName. 16 | func (hs HistogramSeries) GetName() string { 17 | return hs.Name 18 | } 19 | 20 | // GetStyle implements Series.GetStyle. 21 | func (hs HistogramSeries) GetStyle() Style { 22 | return hs.Style 23 | } 24 | 25 | // GetYAxis returns which yaxis the series is mapped to. 26 | func (hs HistogramSeries) GetYAxis() YAxisType { 27 | return hs.YAxis 28 | } 29 | 30 | // Len implements BoundedValuesProvider.Len. 31 | func (hs HistogramSeries) Len() int { 32 | return hs.InnerSeries.Len() 33 | } 34 | 35 | // GetValues implements ValuesProvider.GetValues. 36 | func (hs HistogramSeries) GetValues(index int) (x, y float64) { 37 | return hs.InnerSeries.GetValues(index) 38 | } 39 | 40 | // GetBoundedValues implements BoundedValuesProvider.GetBoundedValue 41 | func (hs HistogramSeries) GetBoundedValues(index int) (x, y1, y2 float64) { 42 | vx, vy := hs.InnerSeries.GetValues(index) 43 | 44 | x = vx 45 | 46 | if vy > 0 { 47 | y1 = vy 48 | return 49 | } 50 | 51 | y2 = vy 52 | return 53 | } 54 | 55 | // Render implements Series.Render. 56 | func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 57 | style := hs.Style.InheritFrom(defaults) 58 | Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs) 59 | } 60 | 61 | // Validate validates the series. 62 | func (hs HistogramSeries) Validate() error { 63 | if hs.InnerSeries == nil { 64 | return fmt.Errorf("histogram series requires InnerSeries to be set") 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /histogram_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestHistogramSeries(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | cs := ContinuousSeries{ 13 | Name: "Test Series", 14 | XValues: LinearRange(1.0, 20.0), 15 | YValues: LinearRange(10.0, -10.0), 16 | } 17 | 18 | hs := HistogramSeries{ 19 | InnerSeries: cs, 20 | } 21 | 22 | for x := 0; x < hs.Len(); x++ { 23 | csx, csy := cs.GetValues(0) 24 | hsx, hsy1, hsy2 := hs.GetBoundedValues(0) 25 | testutil.AssertEqual(t, csx, hsx) 26 | testutil.AssertTrue(t, hsy1 > 0) 27 | testutil.AssertTrue(t, hsy2 <= 0) 28 | testutil.AssertTrue(t, csy < 0 || (csy > 0 && csy == hsy1)) 29 | testutil.AssertTrue(t, csy > 0 || (csy < 0 && csy == hsy2)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /image_writer.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "image" 7 | "image/png" 8 | ) 9 | 10 | // RGBACollector is a render target for a chart. 11 | type RGBACollector interface { 12 | SetRGBA(i *image.RGBA) 13 | } 14 | 15 | // ImageWriter is a special type of io.Writer that produces a final image. 16 | type ImageWriter struct { 17 | rgba *image.RGBA 18 | contents *bytes.Buffer 19 | } 20 | 21 | func (ir *ImageWriter) Write(buffer []byte) (int, error) { 22 | if ir.contents == nil { 23 | ir.contents = bytes.NewBuffer([]byte{}) 24 | } 25 | return ir.contents.Write(buffer) 26 | } 27 | 28 | // SetRGBA sets a raw version of the image. 29 | func (ir *ImageWriter) SetRGBA(i *image.RGBA) { 30 | ir.rgba = i 31 | } 32 | 33 | // Image returns an *image.Image for the result. 34 | func (ir *ImageWriter) Image() (image.Image, error) { 35 | if ir.rgba != nil { 36 | return ir.rgba, nil 37 | } 38 | if ir.contents != nil && ir.contents.Len() > 0 { 39 | return png.Decode(ir.contents) 40 | } 41 | return nil, errors.New("no valid sources for image data, cannot continue") 42 | } 43 | -------------------------------------------------------------------------------- /jet.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "github.com/wcharczuk/go-chart/v2/drawing" 4 | 5 | // Jet is a color map provider based on matlab's jet color map. 6 | func Jet(v, vmin, vmax float64) drawing.Color { 7 | c := drawing.Color{R: 0xff, G: 0xff, B: 0xff, A: 0xff} // white 8 | var dv float64 9 | 10 | if v < vmin { 11 | v = vmin 12 | } 13 | if v > vmax { 14 | v = vmax 15 | } 16 | dv = vmax - vmin 17 | 18 | if v < (vmin + 0.25*dv) { 19 | c.R = 0 20 | c.G = drawing.ColorChannelFromFloat(4 * (v - vmin) / dv) 21 | } else if v < (vmin + 0.5*dv) { 22 | c.R = 0 23 | c.B = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.25*dv-v)/dv) 24 | } else if v < (vmin + 0.75*dv) { 25 | c.R = drawing.ColorChannelFromFloat(4 * (v - vmin - 0.5*dv) / dv) 26 | c.B = 0 27 | } else { 28 | c.G = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.75*dv-v)/dv) 29 | c.B = 0 30 | } 31 | 32 | return c 33 | } 34 | -------------------------------------------------------------------------------- /last_value_annotation_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "fmt" 4 | 5 | // LastValueAnnotationSeries returns an annotation series of just the last value of a value provider. 6 | func LastValueAnnotationSeries(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { 7 | var vf ValueFormatter 8 | if len(vfs) > 0 { 9 | vf = vfs[0] 10 | } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { 11 | _, vf = typed.GetValueFormatters() 12 | } else { 13 | vf = FloatValueFormatter 14 | } 15 | 16 | var lastValue Value2 17 | if typed, isTyped := innerSeries.(LastValuesProvider); isTyped { 18 | lastValue.XValue, lastValue.YValue = typed.GetLastValues() 19 | lastValue.Label = vf(lastValue.YValue) 20 | } else { 21 | lastValue.XValue, lastValue.YValue = innerSeries.GetValues(innerSeries.Len() - 1) 22 | lastValue.Label = vf(lastValue.YValue) 23 | } 24 | 25 | var seriesName string 26 | var seriesStyle Style 27 | if typed, isTyped := innerSeries.(Series); isTyped { 28 | seriesName = fmt.Sprintf("%s - Last Value", typed.GetName()) 29 | seriesStyle = typed.GetStyle() 30 | } 31 | 32 | return AnnotationSeries{ 33 | Name: seriesName, 34 | Style: seriesStyle, 35 | Annotations: []Value2{lastValue}, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /last_value_annotation_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestLastValueAnnotationSeries(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | series := ContinuousSeries{ 13 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 14 | YValues: []float64{5.0, 3.0, 3.0, 2.0, 1.0}, 15 | } 16 | 17 | lva := LastValueAnnotationSeries(series) 18 | testutil.AssertNotEmpty(t, lva.Annotations) 19 | lvaa := lva.Annotations[0] 20 | testutil.AssertEqual(t, 5, lvaa.XValue) 21 | testutil.AssertEqual(t, 1, lvaa.YValue) 22 | } 23 | -------------------------------------------------------------------------------- /legend_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestLegend(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | graph := Chart{ 14 | Series: []Series{ 15 | ContinuousSeries{ 16 | Name: "A test series", 17 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 18 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 19 | }, 20 | }, 21 | } 22 | 23 | //note we have to do this as a separate step because we need a reference to graph 24 | graph.Elements = []Renderable{ 25 | Legend(&graph), 26 | } 27 | buf := bytes.NewBuffer([]byte{}) 28 | err := graph.Render(PNG, buf) 29 | testutil.AssertNil(t, err) 30 | testutil.AssertNotZero(t, buf.Len()) 31 | } 32 | -------------------------------------------------------------------------------- /linear_coefficient_provider.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // LinearCoefficientProvider is a type that returns linear cofficients. 4 | type LinearCoefficientProvider interface { 5 | Coefficients() (m, b, stdev, avg float64) 6 | } 7 | 8 | // LinearCoefficients returns a fixed linear coefficient pair. 9 | func LinearCoefficients(m, b float64) LinearCoefficientSet { 10 | return LinearCoefficientSet{ 11 | M: m, 12 | B: b, 13 | } 14 | } 15 | 16 | // NormalizedLinearCoefficients returns a fixed linear coefficient pair. 17 | func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet { 18 | return LinearCoefficientSet{ 19 | M: m, 20 | B: b, 21 | StdDev: stdev, 22 | Avg: avg, 23 | } 24 | } 25 | 26 | // LinearCoefficientSet is the m and b values for the linear equation in the form: 27 | // y = (m*x) + b 28 | type LinearCoefficientSet struct { 29 | M float64 30 | B float64 31 | StdDev float64 32 | Avg float64 33 | } 34 | 35 | // Coefficients returns the coefficients. 36 | func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) { 37 | m = lcs.M 38 | b = lcs.B 39 | stdev = lcs.StdDev 40 | avg = lcs.Avg 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /linear_regression_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestLinearRegressionSeries(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | mainSeries := ContinuousSeries{ 13 | Name: "A test series", 14 | XValues: LinearRange(1.0, 100.0), 15 | YValues: LinearRange(1.0, 100.0), 16 | } 17 | 18 | linRegSeries := &LinearRegressionSeries{ 19 | InnerSeries: mainSeries, 20 | } 21 | 22 | lrx0, lry0 := linRegSeries.GetValues(0) 23 | testutil.AssertInDelta(t, 1.0, lrx0, 0.0000001) 24 | testutil.AssertInDelta(t, 1.0, lry0, 0.0000001) 25 | 26 | lrxn, lryn := linRegSeries.GetLastValues() 27 | testutil.AssertInDelta(t, 100.0, lrxn, 0.0000001) 28 | testutil.AssertInDelta(t, 100.0, lryn, 0.0000001) 29 | } 30 | 31 | func TestLinearRegressionSeriesDesc(t *testing.T) { 32 | // replaced new assertions helper 33 | 34 | mainSeries := ContinuousSeries{ 35 | Name: "A test series", 36 | XValues: LinearRange(100.0, 1.0), 37 | YValues: LinearRange(100.0, 1.0), 38 | } 39 | 40 | linRegSeries := &LinearRegressionSeries{ 41 | InnerSeries: mainSeries, 42 | } 43 | 44 | lrx0, lry0 := linRegSeries.GetValues(0) 45 | testutil.AssertInDelta(t, 100.0, lrx0, 0.0000001) 46 | testutil.AssertInDelta(t, 100.0, lry0, 0.0000001) 47 | 48 | lrxn, lryn := linRegSeries.GetLastValues() 49 | testutil.AssertInDelta(t, 1.0, lrxn, 0.0000001) 50 | testutil.AssertInDelta(t, 1.0, lryn, 0.0000001) 51 | } 52 | 53 | func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { 54 | // replaced new assertions helper 55 | 56 | mainSeries := ContinuousSeries{ 57 | Name: "A test series", 58 | XValues: LinearRange(100.0, 1.0), 59 | YValues: LinearRange(100.0, 1.0), 60 | } 61 | 62 | linRegSeries := &LinearRegressionSeries{ 63 | InnerSeries: mainSeries, 64 | Offset: 10, 65 | Limit: 10, 66 | } 67 | 68 | testutil.AssertEqual(t, 10, linRegSeries.Len()) 69 | 70 | lrx0, lry0 := linRegSeries.GetValues(0) 71 | testutil.AssertInDelta(t, 90.0, lrx0, 0.0000001) 72 | testutil.AssertInDelta(t, 90.0, lry0, 0.0000001) 73 | 74 | lrxn, lryn := linRegSeries.GetLastValues() 75 | testutil.AssertInDelta(t, 80.0, lrxn, 0.0000001) 76 | testutil.AssertInDelta(t, 80.0, lryn, 0.0000001) 77 | } 78 | -------------------------------------------------------------------------------- /linear_sequence.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // LinearRange returns an array of values representing the range from start to end, incremented by 1.0. 4 | func LinearRange(start, end float64) []float64 { 5 | return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(1.0)}.Values() 6 | } 7 | 8 | // LinearRangeWithStep returns the array values of a linear seq with a given start, end and optional step. 9 | func LinearRangeWithStep(start, end, step float64) []float64 { 10 | return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(step)}.Values() 11 | } 12 | 13 | // NewLinearSequence returns a new linear generator. 14 | func NewLinearSequence() *LinearSeq { 15 | return &LinearSeq{step: 1.0} 16 | } 17 | 18 | // LinearSeq is a stepwise generator. 19 | type LinearSeq struct { 20 | start float64 21 | end float64 22 | step float64 23 | } 24 | 25 | // Start returns the start value. 26 | func (lg LinearSeq) Start() float64 { 27 | return lg.start 28 | } 29 | 30 | // End returns the end value. 31 | func (lg LinearSeq) End() float64 { 32 | return lg.end 33 | } 34 | 35 | // Step returns the step value. 36 | func (lg LinearSeq) Step() float64 { 37 | return lg.step 38 | } 39 | 40 | // Len returns the number of elements in the seq. 41 | func (lg LinearSeq) Len() int { 42 | if lg.start < lg.end { 43 | return int((lg.end-lg.start)/lg.step) + 1 44 | } 45 | return int((lg.start-lg.end)/lg.step) + 1 46 | } 47 | 48 | // GetValue returns the value at a given index. 49 | func (lg LinearSeq) GetValue(index int) float64 { 50 | fi := float64(index) 51 | if lg.start < lg.end { 52 | return lg.start + (fi * lg.step) 53 | } 54 | return lg.start - (fi * lg.step) 55 | } 56 | 57 | // WithStart sets the start and returns the linear generator. 58 | func (lg *LinearSeq) WithStart(start float64) *LinearSeq { 59 | lg.start = start 60 | return lg 61 | } 62 | 63 | // WithEnd sets the end and returns the linear generator. 64 | func (lg *LinearSeq) WithEnd(end float64) *LinearSeq { 65 | lg.end = end 66 | return lg 67 | } 68 | 69 | // WithStep sets the step and returns the linear generator. 70 | func (lg *LinearSeq) WithStep(step float64) *LinearSeq { 71 | lg.step = step 72 | return lg 73 | } 74 | -------------------------------------------------------------------------------- /linear_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Interface Assertions. 8 | var ( 9 | _ Series = (*LinearSeries)(nil) 10 | _ FirstValuesProvider = (*LinearSeries)(nil) 11 | _ LastValuesProvider = (*LinearSeries)(nil) 12 | ) 13 | 14 | // LinearSeries is a series that plots a line in a given domain. 15 | type LinearSeries struct { 16 | Name string 17 | Style Style 18 | YAxis YAxisType 19 | 20 | XValues []float64 21 | InnerSeries LinearCoefficientProvider 22 | 23 | m float64 24 | b float64 25 | stdev float64 26 | avg float64 27 | } 28 | 29 | // GetName returns the name of the time series. 30 | func (ls LinearSeries) GetName() string { 31 | return ls.Name 32 | } 33 | 34 | // GetStyle returns the line style. 35 | func (ls LinearSeries) GetStyle() Style { 36 | return ls.Style 37 | } 38 | 39 | // GetYAxis returns which YAxis the series draws on. 40 | func (ls LinearSeries) GetYAxis() YAxisType { 41 | return ls.YAxis 42 | } 43 | 44 | // Len returns the number of elements in the series. 45 | func (ls LinearSeries) Len() int { 46 | return len(ls.XValues) 47 | } 48 | 49 | // GetEndIndex returns the effective limit end. 50 | func (ls LinearSeries) GetEndIndex() int { 51 | return len(ls.XValues) - 1 52 | } 53 | 54 | // GetValues gets a value at a given index. 55 | func (ls *LinearSeries) GetValues(index int) (x, y float64) { 56 | if ls.InnerSeries == nil || len(ls.XValues) == 0 { 57 | return 58 | } 59 | if ls.IsZero() { 60 | ls.computeCoefficients() 61 | } 62 | x = ls.XValues[index] 63 | y = (ls.m * ls.normalize(x)) + ls.b 64 | return 65 | } 66 | 67 | // GetFirstValues computes the first linear regression value. 68 | func (ls *LinearSeries) GetFirstValues() (x, y float64) { 69 | if ls.InnerSeries == nil || len(ls.XValues) == 0 { 70 | return 71 | } 72 | if ls.IsZero() { 73 | ls.computeCoefficients() 74 | } 75 | x, y = ls.GetValues(0) 76 | return 77 | } 78 | 79 | // GetLastValues computes the last linear regression value. 80 | func (ls *LinearSeries) GetLastValues() (x, y float64) { 81 | if ls.InnerSeries == nil || len(ls.XValues) == 0 { 82 | return 83 | } 84 | if ls.IsZero() { 85 | ls.computeCoefficients() 86 | } 87 | x, y = ls.GetValues(ls.GetEndIndex()) 88 | return 89 | } 90 | 91 | // Render renders the series. 92 | func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 93 | Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls) 94 | } 95 | 96 | // Validate validates the series. 97 | func (ls LinearSeries) Validate() error { 98 | if ls.InnerSeries == nil { 99 | return fmt.Errorf("linear regression series requires InnerSeries to be set") 100 | } 101 | return nil 102 | } 103 | 104 | // IsZero returns if the linear series has computed coefficients or not. 105 | func (ls LinearSeries) IsZero() bool { 106 | return ls.m == 0 && ls.b == 0 107 | } 108 | 109 | // computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`. 110 | func (ls *LinearSeries) computeCoefficients() { 111 | ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients() 112 | } 113 | 114 | func (ls *LinearSeries) normalize(xvalue float64) float64 { 115 | if ls.avg > 0 && ls.stdev > 0 { 116 | return (xvalue - ls.avg) / ls.stdev 117 | } 118 | return xvalue 119 | } 120 | -------------------------------------------------------------------------------- /logarithmic_range.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // LogarithmicRange represents a boundary for a set of numbers. 9 | type LogarithmicRange struct { 10 | Min float64 11 | Max float64 12 | Domain int 13 | Descending bool 14 | } 15 | 16 | // IsDescending returns if the range is descending. 17 | func (r LogarithmicRange) IsDescending() bool { 18 | return r.Descending 19 | } 20 | 21 | // IsZero returns if the LogarithmicRange has been set or not. 22 | func (r LogarithmicRange) IsZero() bool { 23 | return (r.Min == 0 || math.IsNaN(r.Min)) && 24 | (r.Max == 0 || math.IsNaN(r.Max)) && 25 | r.Domain == 0 26 | } 27 | 28 | // GetMin gets the min value for the continuous range. 29 | func (r LogarithmicRange) GetMin() float64 { 30 | return r.Min 31 | } 32 | 33 | // SetMin sets the min value for the continuous range. 34 | func (r *LogarithmicRange) SetMin(min float64) { 35 | r.Min = min 36 | } 37 | 38 | // GetMax returns the max value for the continuous range. 39 | func (r LogarithmicRange) GetMax() float64 { 40 | return r.Max 41 | } 42 | 43 | // SetMax sets the max value for the continuous range. 44 | func (r *LogarithmicRange) SetMax(max float64) { 45 | r.Max = max 46 | } 47 | 48 | // GetDelta returns the difference between the min and max value. 49 | func (r LogarithmicRange) GetDelta() float64 { 50 | return r.Max - r.Min 51 | } 52 | 53 | // GetDomain returns the range domain. 54 | func (r LogarithmicRange) GetDomain() int { 55 | return r.Domain 56 | } 57 | 58 | // SetDomain sets the range domain. 59 | func (r *LogarithmicRange) SetDomain(domain int) { 60 | r.Domain = domain 61 | } 62 | 63 | // String returns a simple string for the LogarithmicRange. 64 | func (r LogarithmicRange) String() string { 65 | return fmt.Sprintf("LogarithmicRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) 66 | } 67 | 68 | // Translate maps a given value into the LogarithmicRange space. Modified version from ContinuousRange. 69 | func (r LogarithmicRange) Translate(value float64) int { 70 | if value < 1 { 71 | return 0 72 | } 73 | normalized := math.Max(value-r.Min, 1) 74 | ratio := math.Log10(normalized) / math.Log10(r.GetDelta()) 75 | 76 | if r.IsDescending() { 77 | return r.Domain - int(math.Ceil(ratio*float64(r.Domain))) 78 | } 79 | 80 | return int(math.Ceil(ratio * float64(r.Domain))) 81 | } 82 | 83 | // GetTicks calculates the needed ticks for the axis, in log scale. Only supports Y values > 0. 84 | func (r LogarithmicRange) GetTicks(render Renderer, defaults Style, vf ValueFormatter) []Tick { 85 | var ticks []Tick 86 | exponentStart := int64(math.Max(0, math.Floor(math.Log10(r.Min)))) // one below min 87 | exponentEnd := int64(math.Max(0, math.Ceil(math.Log10(r.Max)))) // one above max 88 | for exp:=exponentStart; exp<=exponentEnd; exp++ { 89 | tickVal := math.Pow(10, float64(exp)) 90 | ticks = append(ticks, Tick{Value: tickVal, Label: vf(tickVal)}) 91 | } 92 | 93 | return ticks 94 | } 95 | -------------------------------------------------------------------------------- /logarithmic_range_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestLogRangeTranslate(t *testing.T) { 10 | values := []float64{1, 10, 100, 1000, 10000, 100000, 1000000} 11 | r := LogarithmicRange{Domain: 1000} 12 | r.Min, r.Max = MinMax(values...) 13 | 14 | testutil.AssertEqual(t, 0, r.Translate(0)) // goes to bottom 15 | testutil.AssertEqual(t, 0, r.Translate(1)) // goes to bottom 16 | testutil.AssertEqual(t, 160, r.Translate(10)) // roughly 1/6th of max 17 | testutil.AssertEqual(t, 500, r.Translate(1000)) // roughly 1/2 of max (1.0e6 / 1.0e3) 18 | testutil.AssertEqual(t, 1000, r.Translate(1000000)) // max value 19 | } 20 | 21 | func TestGetTicks(t *testing.T) { 22 | values := []float64{35, 512, 1525122} 23 | r := LogarithmicRange{Domain: 1000} 24 | r.Min, r.Max = MinMax(values...) 25 | 26 | ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) 27 | testutil.AssertEqual(t, 7, len(ticks)) 28 | testutil.AssertEqual(t, 10, ticks[0].Value) 29 | testutil.AssertEqual(t, 100, ticks[1].Value) 30 | testutil.AssertEqual(t, 10000000, ticks[6].Value) 31 | } 32 | 33 | func TestGetTicksFromHigh(t *testing.T) { 34 | values := []float64{1412, 352144, 1525122} // min tick should be 1000 35 | r := LogarithmicRange{} 36 | r.Min, r.Max = MinMax(values...) 37 | 38 | ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) 39 | testutil.AssertEqual(t, 5, len(ticks)) 40 | testutil.AssertEqual(t, float64(1000), ticks[0].Value) 41 | testutil.AssertEqual(t, float64(10000), ticks[1].Value) 42 | testutil.AssertEqual(t, float64(10000000), ticks[4].Value) 43 | } 44 | -------------------------------------------------------------------------------- /macd_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | var ( 11 | macdExpected = []float64{ 12 | 0, 13 | 0.06381766382, 14 | 0.1641441222, 15 | 0.2817201894, 16 | 0.4033023481, 17 | 0.3924673744, 18 | 0.2983093823, 19 | 0.1561821464, 20 | -0.008916708129, 21 | -0.05210332292, 22 | -0.01649503993, 23 | 0.06667130899, 24 | 0.1751344574, 25 | 0.1657328378, 26 | 0.08257097469, 27 | -0.04265109369, 28 | -0.1875741257, 29 | -0.2091853882, 30 | -0.1518975486, 31 | -0.04781419838, 32 | 0.08025242841, 33 | 0.08881960494, 34 | 0.02183529775, 35 | -0.08904155476, 36 | -0.2214141128, 37 | -0.2321805992, 38 | -0.1656331722, 39 | -0.05373789678, 40 | 0.08083727586, 41 | 0.09475354363, 42 | 0.03209767112, 43 | -0.07534076818, 44 | -0.2050442354, 45 | -0.2138010557, 46 | -0.1458045181, 47 | -0.03293263556, 48 | 0.1022243734, 49 | 0.1163957964, 50 | 0.05372761902, 51 | -0.05393941791, 52 | -0.1840438454, 53 | -0.1933365048, 54 | -0.1259788988, 55 | -0.01382225715, 56 | 0.1205656194, 57 | 0.1339326478, 58 | 0.07044017167, 59 | -0.03805851969, 60 | -0.1689918111, 61 | -0.1791024416, 62 | } 63 | ) 64 | 65 | func TestMACDSeries(t *testing.T) { 66 | // replaced new assertions helper 67 | 68 | mockSeries := mockValuesProvider{ 69 | emaXValues, 70 | emaYValues, 71 | } 72 | testutil.AssertEqual(t, 50, mockSeries.Len()) 73 | 74 | mas := &MACDSeries{ 75 | InnerSeries: mockSeries, 76 | } 77 | 78 | var yvalues []float64 79 | for x := 0; x < mas.Len(); x++ { 80 | _, y := mas.GetValues(x) 81 | yvalues = append(yvalues, y) 82 | } 83 | 84 | testutil.AssertNotEmpty(t, yvalues) 85 | for index, vy := range yvalues { 86 | testutil.AssertInDelta(t, vy, macdExpected[index], emaDelta, fmt.Sprintf("delta @ %d actual: %0.9f expected: %0.9f", index, vy, macdExpected[index])) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /matrix/regression.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrPolyRegArraysSameLength is a common error. 7 | ErrPolyRegArraysSameLength = errors.New("polynomial array inputs must be the same length") 8 | ) 9 | 10 | // Poly returns the polynomial regress of a given degree over the given values. 11 | func Poly(xvalues, yvalues []float64, degree int) ([]float64, error) { 12 | if len(xvalues) != len(yvalues) { 13 | return nil, ErrPolyRegArraysSameLength 14 | } 15 | 16 | m := len(yvalues) 17 | n := degree + 1 18 | y := New(m, 1, yvalues...) 19 | x := Zero(m, n) 20 | 21 | for i := 0; i < m; i++ { 22 | ip := float64(1) 23 | for j := 0; j < n; j++ { 24 | x.Set(i, j, ip) 25 | ip *= xvalues[i] 26 | } 27 | } 28 | 29 | q, r := x.QR() 30 | qty, err := q.Transpose().Times(y) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | c := make([]float64, n) 36 | for i := n - 1; i >= 0; i-- { 37 | c[i] = qty.Get(i, 0) 38 | for j := i + 1; j < n; j++ { 39 | c[i] -= c[j] * r.Get(i, j) 40 | } 41 | c[i] /= r.Get(i, i) 42 | } 43 | 44 | return c, nil 45 | } 46 | -------------------------------------------------------------------------------- /matrix/regression_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestPoly(t *testing.T) { 10 | // replaced new assertions helper 11 | var xGiven = []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 12 | var yGiven = []float64{1, 6, 17, 34, 57, 86, 121, 162, 209, 262, 321} 13 | var degree = 2 14 | 15 | c, err := Poly(xGiven, yGiven, degree) 16 | testutil.AssertNil(t, err) 17 | testutil.AssertLen(t, c, 3) 18 | 19 | testutil.AssertInDelta(t, c[0], 0.999999999, DefaultEpsilon) 20 | testutil.AssertInDelta(t, c[1], 2, DefaultEpsilon) 21 | testutil.AssertInDelta(t, c[2], 3, DefaultEpsilon) 22 | } 23 | -------------------------------------------------------------------------------- /matrix/util.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | ) 7 | 8 | func minInt(values ...int) int { 9 | min := math.MaxInt32 10 | 11 | for x := 0; x < len(values); x++ { 12 | if values[x] < min { 13 | min = values[x] 14 | } 15 | } 16 | return min 17 | } 18 | 19 | func maxInt(values ...int) int { 20 | max := math.MinInt32 21 | 22 | for x := 0; x < len(values); x++ { 23 | if values[x] > max { 24 | max = values[x] 25 | } 26 | } 27 | return max 28 | } 29 | 30 | func f64s(v float64) string { 31 | return strconv.FormatFloat(v, 'f', -1, 64) 32 | } 33 | 34 | func roundToEpsilon(value, epsilon float64) float64 { 35 | return math.Nextafter(value, value) 36 | } 37 | -------------------------------------------------------------------------------- /matrix/vector.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | // Vector is just an array of values. 4 | type Vector []float64 5 | 6 | // DotProduct returns the dot product of two vectors. 7 | func (v Vector) DotProduct(v2 Vector) (result float64, err error) { 8 | if len(v) != len(v2) { 9 | err = ErrDimensionMismatch 10 | return 11 | } 12 | 13 | for i := 0; i < len(v); i++ { 14 | result = result + (v[i] * v2[i]) 15 | } 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /min_max_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // MinSeries draws a horizontal line at the minimum value of the inner series. 9 | type MinSeries struct { 10 | Name string 11 | Style Style 12 | YAxis YAxisType 13 | InnerSeries ValuesProvider 14 | 15 | minValue *float64 16 | } 17 | 18 | // GetName returns the name of the time series. 19 | func (ms MinSeries) GetName() string { 20 | return ms.Name 21 | } 22 | 23 | // GetStyle returns the line style. 24 | func (ms MinSeries) GetStyle() Style { 25 | return ms.Style 26 | } 27 | 28 | // GetYAxis returns which YAxis the series draws on. 29 | func (ms MinSeries) GetYAxis() YAxisType { 30 | return ms.YAxis 31 | } 32 | 33 | // Len returns the number of elements in the series. 34 | func (ms MinSeries) Len() int { 35 | return ms.InnerSeries.Len() 36 | } 37 | 38 | // GetValues gets a value at a given index. 39 | func (ms *MinSeries) GetValues(index int) (x, y float64) { 40 | ms.ensureMinValue() 41 | x, _ = ms.InnerSeries.GetValues(index) 42 | y = *ms.minValue 43 | return 44 | } 45 | 46 | // Render renders the series. 47 | func (ms *MinSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 48 | style := ms.Style.InheritFrom(defaults) 49 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms) 50 | } 51 | 52 | func (ms *MinSeries) ensureMinValue() { 53 | if ms.minValue == nil { 54 | minValue := math.MaxFloat64 55 | var y float64 56 | for x := 0; x < ms.InnerSeries.Len(); x++ { 57 | _, y = ms.InnerSeries.GetValues(x) 58 | if y < minValue { 59 | minValue = y 60 | } 61 | } 62 | ms.minValue = &minValue 63 | } 64 | } 65 | 66 | // Validate validates the series. 67 | func (ms *MinSeries) Validate() error { 68 | if ms.InnerSeries == nil { 69 | return fmt.Errorf("min series requires InnerSeries to be set") 70 | } 71 | return nil 72 | } 73 | 74 | // MaxSeries draws a horizontal line at the maximum value of the inner series. 75 | type MaxSeries struct { 76 | Name string 77 | Style Style 78 | YAxis YAxisType 79 | InnerSeries ValuesProvider 80 | 81 | maxValue *float64 82 | } 83 | 84 | // GetName returns the name of the time series. 85 | func (ms MaxSeries) GetName() string { 86 | return ms.Name 87 | } 88 | 89 | // GetStyle returns the line style. 90 | func (ms MaxSeries) GetStyle() Style { 91 | return ms.Style 92 | } 93 | 94 | // GetYAxis returns which YAxis the series draws on. 95 | func (ms MaxSeries) GetYAxis() YAxisType { 96 | return ms.YAxis 97 | } 98 | 99 | // Len returns the number of elements in the series. 100 | func (ms MaxSeries) Len() int { 101 | return ms.InnerSeries.Len() 102 | } 103 | 104 | // GetValues gets a value at a given index. 105 | func (ms *MaxSeries) GetValues(index int) (x, y float64) { 106 | ms.ensureMaxValue() 107 | x, _ = ms.InnerSeries.GetValues(index) 108 | y = *ms.maxValue 109 | return 110 | } 111 | 112 | // Render renders the series. 113 | func (ms *MaxSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 114 | style := ms.Style.InheritFrom(defaults) 115 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms) 116 | } 117 | 118 | func (ms *MaxSeries) ensureMaxValue() { 119 | if ms.maxValue == nil { 120 | maxValue := -math.MaxFloat64 121 | var y float64 122 | for x := 0; x < ms.InnerSeries.Len(); x++ { 123 | _, y = ms.InnerSeries.GetValues(x) 124 | if y > maxValue { 125 | maxValue = y 126 | } 127 | } 128 | ms.maxValue = &maxValue 129 | } 130 | } 131 | 132 | // Validate validates the series. 133 | func (ms *MaxSeries) Validate() error { 134 | if ms.InnerSeries == nil { 135 | return fmt.Errorf("max series requires InnerSeries to be set") 136 | } 137 | return nil 138 | } 139 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // ParseFloats parses a list of floats. 10 | func ParseFloats(values ...string) ([]float64, error) { 11 | var output []float64 12 | var parsedValue float64 13 | var err error 14 | var cleaned string 15 | for _, value := range values { 16 | cleaned = strings.TrimSpace(strings.Replace(value, ",", "", -1)) 17 | if cleaned == "" { 18 | continue 19 | } 20 | if parsedValue, err = strconv.ParseFloat(cleaned, 64); err != nil { 21 | return nil, err 22 | } 23 | output = append(output, parsedValue) 24 | } 25 | return output, nil 26 | } 27 | 28 | // ParseTimes parses a list of times with a given format. 29 | func ParseTimes(layout string, values ...string) ([]time.Time, error) { 30 | var output []time.Time 31 | var parsedValue time.Time 32 | var err error 33 | for _, value := range values { 34 | if parsedValue, err = time.Parse(layout, value); err != nil { 35 | return nil, err 36 | } 37 | output = append(output, parsedValue) 38 | } 39 | return output, nil 40 | } 41 | -------------------------------------------------------------------------------- /percent_change_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // Interface Assertions. 4 | var ( 5 | _ Series = (*PercentChangeSeries)(nil) 6 | _ FirstValuesProvider = (*PercentChangeSeries)(nil) 7 | _ LastValuesProvider = (*PercentChangeSeries)(nil) 8 | _ ValueFormatterProvider = (*PercentChangeSeries)(nil) 9 | ) 10 | 11 | // PercentChangeSeriesSource is a series that 12 | // can be used with a PercentChangeSeries 13 | type PercentChangeSeriesSource interface { 14 | Series 15 | FirstValuesProvider 16 | LastValuesProvider 17 | ValuesProvider 18 | ValueFormatterProvider 19 | } 20 | 21 | // PercentChangeSeries applies a 22 | // percentage difference function to a given continuous series. 23 | type PercentChangeSeries struct { 24 | Name string 25 | Style Style 26 | YAxis YAxisType 27 | InnerSeries PercentChangeSeriesSource 28 | } 29 | 30 | // GetName returns the name of the time series. 31 | func (pcs PercentChangeSeries) GetName() string { 32 | return pcs.Name 33 | } 34 | 35 | // GetStyle returns the line style. 36 | func (pcs PercentChangeSeries) GetStyle() Style { 37 | return pcs.Style 38 | } 39 | 40 | // Len implements part of Series. 41 | func (pcs PercentChangeSeries) Len() int { 42 | return pcs.InnerSeries.Len() 43 | } 44 | 45 | // GetFirstValues implements FirstValuesProvider. 46 | func (pcs PercentChangeSeries) GetFirstValues() (x, y float64) { 47 | return pcs.InnerSeries.GetFirstValues() 48 | } 49 | 50 | // GetValues gets x, y values at a given index. 51 | func (pcs PercentChangeSeries) GetValues(index int) (x, y float64) { 52 | _, fy := pcs.InnerSeries.GetFirstValues() 53 | x0, y0 := pcs.InnerSeries.GetValues(index) 54 | x = x0 55 | y = PercentDifference(fy, y0) 56 | return 57 | } 58 | 59 | // GetValueFormatters returns value formatter defaults for the series. 60 | func (pcs PercentChangeSeries) GetValueFormatters() (x, y ValueFormatter) { 61 | x, _ = pcs.InnerSeries.GetValueFormatters() 62 | y = PercentValueFormatter 63 | return 64 | } 65 | 66 | // GetYAxis returns which YAxis the series draws on. 67 | func (pcs PercentChangeSeries) GetYAxis() YAxisType { 68 | return pcs.YAxis 69 | } 70 | 71 | // GetLastValues gets the last values. 72 | func (pcs PercentChangeSeries) GetLastValues() (x, y float64) { 73 | _, fy := pcs.InnerSeries.GetFirstValues() 74 | x0, y0 := pcs.InnerSeries.GetLastValues() 75 | x = x0 76 | y = PercentDifference(fy, y0) 77 | return 78 | } 79 | 80 | // Render renders the series. 81 | func (pcs PercentChangeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 82 | style := pcs.Style.InheritFrom(defaults) 83 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, pcs) 84 | } 85 | 86 | // Validate validates the series. 87 | func (pcs PercentChangeSeries) Validate() error { 88 | return pcs.InnerSeries.Validate() 89 | } 90 | -------------------------------------------------------------------------------- /percent_change_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestPercentageDifferenceSeries(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | cs := ContinuousSeries{ 13 | XValues: LinearRange(1.0, 10.0), 14 | YValues: LinearRange(1.0, 10.0), 15 | } 16 | 17 | pcs := PercentChangeSeries{ 18 | Name: "Test Series", 19 | InnerSeries: cs, 20 | } 21 | 22 | testutil.AssertEqual(t, "Test Series", pcs.GetName()) 23 | testutil.AssertEqual(t, 10, pcs.Len()) 24 | x0, y0 := pcs.GetValues(0) 25 | testutil.AssertEqual(t, 1.0, x0) 26 | testutil.AssertEqual(t, 0, y0) 27 | 28 | xn, yn := pcs.GetValues(9) 29 | testutil.AssertEqual(t, 10.0, xn) 30 | testutil.AssertEqual(t, 9.0, yn) 31 | 32 | xn, yn = pcs.GetLastValues() 33 | testutil.AssertEqual(t, 10.0, xn) 34 | testutil.AssertEqual(t, 9.0, yn) 35 | } 36 | -------------------------------------------------------------------------------- /pie_chart_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestPieChart(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | pie := PieChart{ 14 | Canvas: Style{ 15 | FillColor: ColorLightGray, 16 | }, 17 | Values: []Value{ 18 | {Value: 10, Label: "Blue"}, 19 | {Value: 9, Label: "Green"}, 20 | {Value: 8, Label: "Gray"}, 21 | {Value: 7, Label: "Orange"}, 22 | {Value: 6, Label: "HEANG"}, 23 | {Value: 5, Label: "??"}, 24 | {Value: 2, Label: "!!"}, 25 | }, 26 | } 27 | 28 | b := bytes.NewBuffer([]byte{}) 29 | pie.Render(PNG, b) 30 | testutil.AssertNotZero(t, b.Len()) 31 | } 32 | 33 | func TestPieChartDropsZeroValues(t *testing.T) { 34 | // replaced new assertions helper 35 | 36 | pie := PieChart{ 37 | Canvas: Style{ 38 | FillColor: ColorLightGray, 39 | }, 40 | Values: []Value{ 41 | {Value: 5, Label: "Blue"}, 42 | {Value: 5, Label: "Green"}, 43 | {Value: 0, Label: "Gray"}, 44 | }, 45 | } 46 | 47 | b := bytes.NewBuffer([]byte{}) 48 | err := pie.Render(PNG, b) 49 | testutil.AssertNil(t, err) 50 | } 51 | 52 | func TestPieChartAllZeroValues(t *testing.T) { 53 | // replaced new assertions helper 54 | 55 | pie := PieChart{ 56 | Canvas: Style{ 57 | FillColor: ColorLightGray, 58 | }, 59 | Values: []Value{ 60 | {Value: 0, Label: "Blue"}, 61 | {Value: 0, Label: "Green"}, 62 | {Value: 0, Label: "Gray"}, 63 | }, 64 | } 65 | 66 | b := bytes.NewBuffer([]byte{}) 67 | err := pie.Render(PNG, b) 68 | testutil.AssertNotNil(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /polynomial_regression_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/matrix" 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestPolynomialRegression(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | var xv []float64 14 | var yv []float64 15 | 16 | for i := 0; i < 100; i++ { 17 | xv = append(xv, float64(i)) 18 | yv = append(yv, float64(i*i)) 19 | } 20 | 21 | values := ContinuousSeries{ 22 | XValues: xv, 23 | YValues: yv, 24 | } 25 | 26 | poly := &PolynomialRegressionSeries{ 27 | InnerSeries: values, 28 | Degree: 2, 29 | } 30 | 31 | for i := 0; i < 100; i++ { 32 | _, y := poly.GetValues(i) 33 | testutil.AssertInDelta(t, float64(i*i), y, matrix.DefaultEpsilon) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /random_sequence.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | var ( 10 | _ Sequence = (*RandomSeq)(nil) 11 | ) 12 | 13 | // RandomValues returns an array of random values. 14 | func RandomValues(count int) []float64 { 15 | return Seq{NewRandomSequence().WithLen(count)}.Values() 16 | } 17 | 18 | // RandomValuesWithMax returns an array of random values with a given average. 19 | func RandomValuesWithMax(count int, max float64) []float64 { 20 | return Seq{NewRandomSequence().WithMax(max).WithLen(count)}.Values() 21 | } 22 | 23 | // NewRandomSequence creates a new random seq. 24 | func NewRandomSequence() *RandomSeq { 25 | return &RandomSeq{ 26 | rnd: rand.New(rand.NewSource(time.Now().Unix())), 27 | } 28 | } 29 | 30 | // RandomSeq is a random number seq generator. 31 | type RandomSeq struct { 32 | rnd *rand.Rand 33 | max *float64 34 | min *float64 35 | len *int 36 | } 37 | 38 | // Len returns the number of elements that will be generated. 39 | func (r *RandomSeq) Len() int { 40 | if r.len != nil { 41 | return *r.len 42 | } 43 | return math.MaxInt32 44 | } 45 | 46 | // GetValue returns the value. 47 | func (r *RandomSeq) GetValue(_ int) float64 { 48 | if r.min != nil && r.max != nil { 49 | var delta float64 50 | 51 | if *r.max > *r.min { 52 | delta = *r.max - *r.min 53 | } else { 54 | delta = *r.min - *r.max 55 | } 56 | 57 | return *r.min + (r.rnd.Float64() * delta) 58 | } else if r.max != nil { 59 | return r.rnd.Float64() * *r.max 60 | } else if r.min != nil { 61 | return *r.min + (r.rnd.Float64()) 62 | } 63 | return r.rnd.Float64() 64 | } 65 | 66 | // WithLen sets a maximum len 67 | func (r *RandomSeq) WithLen(length int) *RandomSeq { 68 | r.len = &length 69 | return r 70 | } 71 | 72 | // Min returns the minimum value. 73 | func (r RandomSeq) Min() *float64 { 74 | return r.min 75 | } 76 | 77 | // WithMin sets the scale and returns the Random. 78 | func (r *RandomSeq) WithMin(min float64) *RandomSeq { 79 | r.min = &min 80 | return r 81 | } 82 | 83 | // Max returns the maximum value. 84 | func (r RandomSeq) Max() *float64 { 85 | return r.max 86 | } 87 | 88 | // WithMax sets the average and returns the Random. 89 | func (r *RandomSeq) WithMax(max float64) *RandomSeq { 90 | r.max = &max 91 | return r 92 | } 93 | -------------------------------------------------------------------------------- /range.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // NameProvider is a type that returns a name. 4 | type NameProvider interface { 5 | GetName() string 6 | } 7 | 8 | // StyleProvider is a type that returns a style. 9 | type StyleProvider interface { 10 | GetStyle() Style 11 | } 12 | 13 | // IsZeroable is a type that returns if it's been set or not. 14 | type IsZeroable interface { 15 | IsZero() bool 16 | } 17 | 18 | // Stringable is a type that has a string representation. 19 | type Stringable interface { 20 | String() string 21 | } 22 | 23 | // Range is a common interface for a range of values. 24 | type Range interface { 25 | Stringable 26 | IsZeroable 27 | 28 | GetMin() float64 29 | SetMin(min float64) 30 | 31 | GetMax() float64 32 | SetMax(max float64) 33 | 34 | GetDelta() float64 35 | 36 | GetDomain() int 37 | SetDomain(domain int) 38 | 39 | IsDescending() bool 40 | 41 | // Translate the range to the domain. 42 | Translate(value float64) int 43 | } 44 | -------------------------------------------------------------------------------- /renderable.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // Renderable is a function that can be called to render custom elements on the chart. 4 | type Renderable func(r Renderer, canvasBox Box, defaults Style) 5 | -------------------------------------------------------------------------------- /renderer.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/golang/freetype/truetype" 7 | "github.com/wcharczuk/go-chart/v2/drawing" 8 | ) 9 | 10 | // Renderer represents the basic methods required to draw a chart. 11 | type Renderer interface { 12 | // ResetStyle should reset any style related settings on the renderer. 13 | ResetStyle() 14 | 15 | // GetDPI gets the DPI for the renderer. 16 | GetDPI() float64 17 | 18 | // SetDPI sets the DPI for the renderer. 19 | SetDPI(dpi float64) 20 | 21 | // SetClassName sets the current class name. 22 | SetClassName(string) 23 | 24 | // SetStrokeColor sets the current stroke color. 25 | SetStrokeColor(drawing.Color) 26 | 27 | // SetFillColor sets the current fill color. 28 | SetFillColor(drawing.Color) 29 | 30 | // SetStrokeWidth sets the stroke width. 31 | SetStrokeWidth(width float64) 32 | 33 | // SetStrokeDashArray sets the stroke dash array. 34 | SetStrokeDashArray(dashArray []float64) 35 | 36 | // MoveTo moves the cursor to a given point. 37 | MoveTo(x, y int) 38 | 39 | // LineTo both starts a shape and draws a line to a given point 40 | // from the previous point. 41 | LineTo(x, y int) 42 | 43 | // QuadCurveTo draws a quad curve. 44 | // cx and cy represent the bezier "control points". 45 | QuadCurveTo(cx, cy, x, y int) 46 | 47 | // ArcTo draws an arc with a given center (cx,cy) 48 | // a given set of radii (rx,ry), a startAngle and delta (in radians). 49 | ArcTo(cx, cy int, rx, ry, startAngle, delta float64) 50 | 51 | // Close finalizes a shape as drawn by LineTo. 52 | Close() 53 | 54 | // Stroke strokes the path. 55 | Stroke() 56 | 57 | // Fill fills the path, but does not stroke. 58 | Fill() 59 | 60 | // FillStroke fills and strokes a path. 61 | FillStroke() 62 | 63 | // Circle draws a circle at the given coords with a given radius. 64 | Circle(radius float64, x, y int) 65 | 66 | // SetFont sets a font for a text field. 67 | SetFont(*truetype.Font) 68 | 69 | // SetFontColor sets a font's color 70 | SetFontColor(drawing.Color) 71 | 72 | // SetFontSize sets the font size for a text field. 73 | SetFontSize(size float64) 74 | 75 | // Text draws a text blob. 76 | Text(body string, x, y int) 77 | 78 | // MeasureText measures text. 79 | MeasureText(body string) Box 80 | 81 | // SetTextRotatation sets a rotation for drawing elements. 82 | SetTextRotation(radians float64) 83 | 84 | // ClearTextRotation clears rotation. 85 | ClearTextRotation() 86 | 87 | // Save writes the image to the given writer. 88 | Save(w io.Writer) error 89 | } 90 | -------------------------------------------------------------------------------- /renderer_provider.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // RendererProvider is a function that returns a renderer. 4 | type RendererProvider func(int, int) (Renderer, error) 5 | -------------------------------------------------------------------------------- /seq_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestSeqEach(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | values := Seq{NewArray(1, 2, 3, 4)} 13 | values.Each(func(i int, v float64) { 14 | testutil.AssertEqual(t, i, v-1) 15 | }) 16 | } 17 | 18 | func TestSeqMap(t *testing.T) { 19 | // replaced new assertions helper 20 | 21 | values := Seq{NewArray(1, 2, 3, 4)} 22 | mapped := values.Map(func(i int, v float64) float64 { 23 | testutil.AssertEqual(t, i, v-1) 24 | return v * 2 25 | }) 26 | testutil.AssertEqual(t, 4, mapped.Len()) 27 | } 28 | 29 | func TestSeqFoldLeft(t *testing.T) { 30 | // replaced new assertions helper 31 | 32 | values := Seq{NewArray(1, 2, 3, 4)} 33 | ten := values.FoldLeft(func(_ int, vp, v float64) float64 { 34 | return vp + v 35 | }) 36 | testutil.AssertEqual(t, 10, ten) 37 | 38 | orderTest := Seq{NewArray(10, 3, 2, 1)} 39 | four := orderTest.FoldLeft(func(_ int, vp, v float64) float64 { 40 | return vp - v 41 | }) 42 | testutil.AssertEqual(t, 4, four) 43 | } 44 | 45 | func TestSeqFoldRight(t *testing.T) { 46 | // replaced new assertions helper 47 | 48 | values := Seq{NewArray(1, 2, 3, 4)} 49 | ten := values.FoldRight(func(_ int, vp, v float64) float64 { 50 | return vp + v 51 | }) 52 | testutil.AssertEqual(t, 10, ten) 53 | 54 | orderTest := Seq{NewArray(10, 3, 2, 1)} 55 | notFour := orderTest.FoldRight(func(_ int, vp, v float64) float64 { 56 | return vp - v 57 | }) 58 | testutil.AssertEqual(t, -14, notFour) 59 | } 60 | 61 | func TestSeqSum(t *testing.T) { 62 | // replaced new assertions helper 63 | 64 | values := Seq{NewArray(1, 2, 3, 4)} 65 | testutil.AssertEqual(t, 10, values.Sum()) 66 | } 67 | 68 | func TestSeqAverage(t *testing.T) { 69 | // replaced new assertions helper 70 | 71 | values := Seq{NewArray(1, 2, 3, 4)} 72 | testutil.AssertEqual(t, 2.5, values.Average()) 73 | 74 | valuesOdd := Seq{NewArray(1, 2, 3, 4, 5)} 75 | testutil.AssertEqual(t, 3, valuesOdd.Average()) 76 | } 77 | 78 | func TestSequenceVariance(t *testing.T) { 79 | // replaced new assertions helper 80 | 81 | values := Seq{NewArray(1, 2, 3, 4, 5)} 82 | testutil.AssertEqual(t, 2, values.Variance()) 83 | } 84 | 85 | func TestSequenceNormalize(t *testing.T) { 86 | // replaced new assertions helper 87 | 88 | normalized := ValueSequence(1, 2, 3, 4, 5).Normalize().Values() 89 | 90 | testutil.AssertNotEmpty(t, normalized) 91 | testutil.AssertLen(t, normalized, 5) 92 | testutil.AssertEqual(t, 0, normalized[0]) 93 | testutil.AssertEqual(t, 0.25, normalized[1]) 94 | testutil.AssertEqual(t, 1, normalized[4]) 95 | } 96 | 97 | func TestLinearRange(t *testing.T) { 98 | // replaced new assertions helper 99 | 100 | values := LinearRange(1, 100) 101 | testutil.AssertLen(t, values, 100) 102 | testutil.AssertEqual(t, 1, values[0]) 103 | testutil.AssertEqual(t, 100, values[99]) 104 | } 105 | 106 | func TestLinearRangeWithStep(t *testing.T) { 107 | // replaced new assertions helper 108 | 109 | values := LinearRangeWithStep(0, 100, 5) 110 | testutil.AssertEqual(t, 100, values[20]) 111 | testutil.AssertLen(t, values, 21) 112 | } 113 | 114 | func TestLinearRangeReversed(t *testing.T) { 115 | // replaced new assertions helper 116 | 117 | values := LinearRange(10.0, 1.0) 118 | testutil.AssertEqual(t, 10, len(values)) 119 | testutil.AssertEqual(t, 10.0, values[0]) 120 | testutil.AssertEqual(t, 1.0, values[9]) 121 | } 122 | 123 | func TestLinearSequenceRegression(t *testing.T) { 124 | // replaced new assertions helper 125 | 126 | // note; this assumes a 1.0 step is implicitly set in the constructor. 127 | linearProvider := NewLinearSequence().WithStart(1.0).WithEnd(100.0) 128 | testutil.AssertEqual(t, 1, linearProvider.Start()) 129 | testutil.AssertEqual(t, 100, linearProvider.End()) 130 | testutil.AssertEqual(t, 100, linearProvider.Len()) 131 | 132 | values := Seq{linearProvider}.Values() 133 | testutil.AssertLen(t, values, 100) 134 | testutil.AssertEqual(t, 1.0, values[0]) 135 | testutil.AssertEqual(t, 100, values[99]) 136 | } 137 | -------------------------------------------------------------------------------- /series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // Series is an alias to Renderable. 4 | type Series interface { 5 | GetName() string 6 | GetYAxis() YAxisType 7 | GetStyle() Style 8 | Validate() error 9 | Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style) 10 | } 11 | -------------------------------------------------------------------------------- /sma_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | // DefaultSimpleMovingAveragePeriod is the default number of values to average. 9 | DefaultSimpleMovingAveragePeriod = 16 10 | ) 11 | 12 | // Interface Assertions. 13 | var ( 14 | _ Series = (*SMASeries)(nil) 15 | _ FirstValuesProvider = (*SMASeries)(nil) 16 | _ LastValuesProvider = (*SMASeries)(nil) 17 | ) 18 | 19 | // SMASeries is a computed series. 20 | type SMASeries struct { 21 | Name string 22 | Style Style 23 | YAxis YAxisType 24 | 25 | Period int 26 | InnerSeries ValuesProvider 27 | } 28 | 29 | // GetName returns the name of the time series. 30 | func (sma SMASeries) GetName() string { 31 | return sma.Name 32 | } 33 | 34 | // GetStyle returns the line style. 35 | func (sma SMASeries) GetStyle() Style { 36 | return sma.Style 37 | } 38 | 39 | // GetYAxis returns which YAxis the series draws on. 40 | func (sma SMASeries) GetYAxis() YAxisType { 41 | return sma.YAxis 42 | } 43 | 44 | // Len returns the number of elements in the series. 45 | func (sma SMASeries) Len() int { 46 | return sma.InnerSeries.Len() 47 | } 48 | 49 | // GetPeriod returns the window size. 50 | func (sma SMASeries) GetPeriod(defaults ...int) int { 51 | if sma.Period == 0 { 52 | if len(defaults) > 0 { 53 | return defaults[0] 54 | } 55 | return DefaultSimpleMovingAveragePeriod 56 | } 57 | return sma.Period 58 | } 59 | 60 | // GetValues gets a value at a given index. 61 | func (sma SMASeries) GetValues(index int) (x, y float64) { 62 | if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { 63 | return 64 | } 65 | px, _ := sma.InnerSeries.GetValues(index) 66 | x = px 67 | y = sma.getAverage(index) 68 | return 69 | } 70 | 71 | // GetFirstValues computes the first moving average value. 72 | func (sma SMASeries) GetFirstValues() (x, y float64) { 73 | if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { 74 | return 75 | } 76 | px, _ := sma.InnerSeries.GetValues(0) 77 | x = px 78 | y = sma.getAverage(0) 79 | return 80 | } 81 | 82 | // GetLastValues computes the last moving average value but walking back window size samples, 83 | // and recomputing the last moving average chunk. 84 | func (sma SMASeries) GetLastValues() (x, y float64) { 85 | if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { 86 | return 87 | } 88 | seriesLen := sma.InnerSeries.Len() 89 | px, _ := sma.InnerSeries.GetValues(seriesLen - 1) 90 | x = px 91 | y = sma.getAverage(seriesLen - 1) 92 | return 93 | } 94 | 95 | func (sma SMASeries) getAverage(index int) float64 { 96 | period := sma.GetPeriod() 97 | floor := MaxInt(0, index-period) 98 | var accum float64 99 | var count float64 100 | for x := index; x >= floor; x-- { 101 | _, vy := sma.InnerSeries.GetValues(x) 102 | accum += vy 103 | count += 1.0 104 | } 105 | return accum / count 106 | } 107 | 108 | // Render renders the series. 109 | func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 110 | style := sma.Style.InheritFrom(defaults) 111 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma) 112 | } 113 | 114 | // Validate validates the series. 115 | func (sma SMASeries) Validate() error { 116 | if sma.InnerSeries == nil { 117 | return fmt.Errorf("sma series requires InnerSeries to be set") 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /sma_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | type mockValuesProvider struct { 10 | X []float64 11 | Y []float64 12 | } 13 | 14 | func (m mockValuesProvider) Len() int { 15 | return MinInt(len(m.X), len(m.Y)) 16 | } 17 | 18 | func (m mockValuesProvider) GetValues(index int) (x, y float64) { 19 | if index < 0 { 20 | panic("negative index at GetValue()") 21 | } 22 | if index >= MinInt(len(m.X), len(m.Y)) { 23 | panic("index is outside the length of m.X or m.Y") 24 | } 25 | x = m.X[index] 26 | y = m.Y[index] 27 | return 28 | } 29 | 30 | func TestSMASeriesGetValue(t *testing.T) { 31 | // replaced new assertions helper 32 | 33 | mockSeries := mockValuesProvider{ 34 | LinearRange(1.0, 10.0), 35 | LinearRange(10, 1.0), 36 | } 37 | testutil.AssertEqual(t, 10, mockSeries.Len()) 38 | 39 | mas := &SMASeries{ 40 | InnerSeries: mockSeries, 41 | Period: 10, 42 | } 43 | 44 | var yvalues []float64 45 | for x := 0; x < mas.Len(); x++ { 46 | _, y := mas.GetValues(x) 47 | yvalues = append(yvalues, y) 48 | } 49 | 50 | testutil.AssertEqual(t, 10.0, yvalues[0]) 51 | testutil.AssertEqual(t, 9.5, yvalues[1]) 52 | testutil.AssertEqual(t, 9.0, yvalues[2]) 53 | testutil.AssertEqual(t, 8.5, yvalues[3]) 54 | testutil.AssertEqual(t, 8.0, yvalues[4]) 55 | testutil.AssertEqual(t, 7.5, yvalues[5]) 56 | testutil.AssertEqual(t, 7.0, yvalues[6]) 57 | testutil.AssertEqual(t, 6.5, yvalues[7]) 58 | testutil.AssertEqual(t, 6.0, yvalues[8]) 59 | } 60 | 61 | func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { 62 | // replaced new assertions helper 63 | 64 | mockSeries := mockValuesProvider{ 65 | LinearRange(1.0, 10.0), 66 | LinearRange(10, 1.0), 67 | } 68 | testutil.AssertEqual(t, 10, mockSeries.Len()) 69 | 70 | mas := &SMASeries{ 71 | InnerSeries: mockSeries, 72 | Period: 15, 73 | } 74 | 75 | var yvalues []float64 76 | for x := 0; x < mas.Len(); x++ { 77 | _, y := mas.GetValues(x) 78 | yvalues = append(yvalues, y) 79 | } 80 | 81 | lx, ly := mas.GetLastValues() 82 | testutil.AssertEqual(t, 10.0, lx) 83 | testutil.AssertEqual(t, 5.5, ly) 84 | testutil.AssertEqual(t, yvalues[len(yvalues)-1], ly) 85 | } 86 | 87 | func TestSMASeriesGetLastValue(t *testing.T) { 88 | // replaced new assertions helper 89 | 90 | mockSeries := mockValuesProvider{ 91 | LinearRange(1.0, 100.0), 92 | LinearRange(100, 1.0), 93 | } 94 | testutil.AssertEqual(t, 100, mockSeries.Len()) 95 | 96 | mas := &SMASeries{ 97 | InnerSeries: mockSeries, 98 | Period: 10, 99 | } 100 | 101 | var yvalues []float64 102 | for x := 0; x < mas.Len(); x++ { 103 | _, y := mas.GetValues(x) 104 | yvalues = append(yvalues, y) 105 | } 106 | 107 | lx, ly := mas.GetLastValues() 108 | testutil.AssertEqual(t, 100.0, lx) 109 | testutil.AssertEqual(t, 6, ly) 110 | testutil.AssertEqual(t, yvalues[len(yvalues)-1], ly) 111 | } 112 | -------------------------------------------------------------------------------- /stringutil.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "strings" 4 | 5 | // SplitCSV splits a corpus by the `,`, dropping leading or trailing whitespace unless quoted. 6 | func SplitCSV(text string) (output []string) { 7 | if len(text) == 0 { 8 | return 9 | } 10 | 11 | var state int 12 | var word []rune 13 | var opened rune 14 | for _, r := range text { 15 | switch state { 16 | case 0: // word 17 | if isQuote(r) { 18 | opened = r 19 | state = 1 20 | } else if isCSVDelim(r) { 21 | output = append(output, strings.TrimSpace(string(word))) 22 | word = nil 23 | } else { 24 | word = append(word, r) 25 | } 26 | case 1: // we're in a quoted section 27 | if matchesQuote(opened, r) { 28 | state = 0 29 | continue 30 | } 31 | word = append(word, r) 32 | } 33 | } 34 | 35 | if len(word) > 0 { 36 | output = append(output, strings.TrimSpace(string(word))) 37 | } 38 | return 39 | } 40 | 41 | func isCSVDelim(r rune) bool { 42 | return r == rune(',') 43 | } 44 | 45 | func isQuote(r rune) bool { 46 | return r == '"' || r == '\'' || r == '“' || r == '”' || r == '`' 47 | } 48 | 49 | func matchesQuote(a, b rune) bool { 50 | if a == '“' && b == '”' { 51 | return true 52 | } 53 | if a == '”' && b == '“' { 54 | return true 55 | } 56 | return a == b 57 | } 58 | -------------------------------------------------------------------------------- /stringutil_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestSplitCSV(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | testutil.AssertEmpty(t, SplitCSV("")) 13 | testutil.AssertEqual(t, []string{"foo"}, SplitCSV("foo")) 14 | testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV("foo,bar")) 15 | testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV("foo, bar")) 16 | testutil.AssertEqual(t, []string{"foo", "bar"}, SplitCSV(" foo , bar ")) 17 | testutil.AssertEqual(t, []string{"foo", "bar", "baz"}, SplitCSV("foo,bar,baz")) 18 | testutil.AssertEqual(t, []string{"foo", "bar", "baz,buzz"}, SplitCSV("foo,bar,\"baz,buzz\"")) 19 | testutil.AssertEqual(t, []string{"foo", "bar", "baz,'buzz'"}, SplitCSV("foo,bar,\"baz,'buzz'\"")) 20 | testutil.AssertEqual(t, []string{"foo", "bar", "baz,'buzz"}, SplitCSV("foo,bar,\"baz,'buzz\"")) 21 | testutil.AssertEqual(t, []string{"foo", "bar", "baz,\"buzz\""}, SplitCSV("foo,bar,'baz,\"buzz\"'")) 22 | } 23 | -------------------------------------------------------------------------------- /text_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestTextWrapWord(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | r, err := PNG(1024, 1024) 13 | testutil.AssertNil(t, err) 14 | f, err := GetDefaultFont() 15 | testutil.AssertNil(t, err) 16 | 17 | basicTextStyle := Style{Font: f, FontSize: 24} 18 | 19 | output := Text.WrapFitWord(r, "this is a test string", 100, basicTextStyle) 20 | testutil.AssertNotEmpty(t, output) 21 | testutil.AssertLen(t, output, 3) 22 | 23 | for _, line := range output { 24 | basicTextStyle.WriteToRenderer(r) 25 | lineBox := r.MeasureText(line) 26 | testutil.AssertTrue(t, lineBox.Width() < 100, line+": "+lineBox.String()) 27 | } 28 | testutil.AssertEqual(t, "this is", output[0]) 29 | testutil.AssertEqual(t, "a test", output[1]) 30 | testutil.AssertEqual(t, "string", output[2]) 31 | 32 | output = Text.WrapFitWord(r, "foo", 100, basicTextStyle) 33 | testutil.AssertLen(t, output, 1) 34 | testutil.AssertEqual(t, "foo", output[0]) 35 | 36 | // test that it handles newlines. 37 | output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring", 100, basicTextStyle) 38 | testutil.AssertLen(t, output, 5) 39 | 40 | // test that it handles newlines and long lines. 41 | output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring that is very long", 100, basicTextStyle) 42 | testutil.AssertLen(t, output, 8) 43 | } 44 | 45 | func TestTextWrapRune(t *testing.T) { 46 | // replaced new assertions helper 47 | 48 | r, err := PNG(1024, 1024) 49 | testutil.AssertNil(t, err) 50 | f, err := GetDefaultFont() 51 | testutil.AssertNil(t, err) 52 | 53 | basicTextStyle := Style{Font: f, FontSize: 24} 54 | 55 | output := Text.WrapFitRune(r, "this is a test string", 150, basicTextStyle) 56 | testutil.AssertNotEmpty(t, output) 57 | testutil.AssertLen(t, output, 2) 58 | testutil.AssertEqual(t, "this is a t", output[0]) 59 | testutil.AssertEqual(t, "est string", output[1]) 60 | } 61 | -------------------------------------------------------------------------------- /tick.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | ) 8 | 9 | // TicksProvider is a type that provides ticks. 10 | type TicksProvider interface { 11 | GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick 12 | } 13 | 14 | // Tick represents a label on an axis. 15 | type Tick struct { 16 | Value float64 17 | Label string 18 | } 19 | 20 | // Ticks is an array of ticks. 21 | type Ticks []Tick 22 | 23 | // Len returns the length of the ticks set. 24 | func (t Ticks) Len() int { 25 | return len(t) 26 | } 27 | 28 | // Swap swaps two elements. 29 | func (t Ticks) Swap(i, j int) { 30 | t[i], t[j] = t[j], t[i] 31 | } 32 | 33 | // Less returns if i's value is less than j's value. 34 | func (t Ticks) Less(i, j int) bool { 35 | return t[i].Value < t[j].Value 36 | } 37 | 38 | // String returns a string representation of the set of ticks. 39 | func (t Ticks) String() string { 40 | var values []string 41 | for i, tick := range t { 42 | values = append(values, fmt.Sprintf("[%d: %s]", i, tick.Label)) 43 | } 44 | return strings.Join(values, ", ") 45 | } 46 | 47 | // GenerateContinuousTicks generates a set of ticks. 48 | func GenerateContinuousTicks(r Renderer, ra Range, isVertical bool, style Style, vf ValueFormatter) []Tick { 49 | if vf == nil { 50 | vf = FloatValueFormatter 51 | } 52 | 53 | var ticks []Tick 54 | min, max := ra.GetMin(), ra.GetMax() 55 | 56 | if ra.IsDescending() { 57 | ticks = append(ticks, Tick{ 58 | Value: max, 59 | Label: vf(max), 60 | }) 61 | } else { 62 | ticks = append(ticks, Tick{ 63 | Value: min, 64 | Label: vf(min), 65 | }) 66 | } 67 | 68 | minLabel := vf(min) 69 | style.GetTextOptions().WriteToRenderer(r) 70 | labelBox := r.MeasureText(minLabel) 71 | 72 | var tickSize float64 73 | if isVertical { 74 | tickSize = float64(labelBox.Height() + DefaultMinimumTickVerticalSpacing) 75 | } else { 76 | tickSize = float64(labelBox.Width() + DefaultMinimumTickHorizontalSpacing) 77 | } 78 | 79 | domain := float64(ra.GetDomain()) 80 | domainRemainder := domain - (tickSize * 2) 81 | intermediateTickCount := int(math.Floor(float64(domainRemainder) / float64(tickSize))) 82 | 83 | rangeDelta := math.Abs(max - min) 84 | tickStep := rangeDelta / float64(intermediateTickCount) 85 | 86 | roundTo := GetRoundToForDelta(rangeDelta) / 10 87 | intermediateTickCount = MinInt(intermediateTickCount, DefaultTickCountSanityCheck) 88 | 89 | for x := 1; x < intermediateTickCount; x++ { 90 | var tickValue float64 91 | if ra.IsDescending() { 92 | tickValue = max - RoundUp(tickStep*float64(x), roundTo) 93 | } else { 94 | tickValue = min + RoundUp(tickStep*float64(x), roundTo) 95 | } 96 | ticks = append(ticks, Tick{ 97 | Value: tickValue, 98 | Label: vf(tickValue), 99 | }) 100 | } 101 | 102 | if ra.IsDescending() { 103 | ticks = append(ticks, Tick{ 104 | Value: min, 105 | Label: vf(min), 106 | }) 107 | } else { 108 | ticks = append(ticks, Tick{ 109 | Value: max, 110 | Label: vf(max), 111 | }) 112 | } 113 | 114 | return ticks 115 | } 116 | -------------------------------------------------------------------------------- /tick_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestGenerateContinuousTicks(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | f, err := GetDefaultFont() 13 | testutil.AssertNil(t, err) 14 | 15 | r, err := PNG(1024, 1024) 16 | testutil.AssertNil(t, err) 17 | r.SetFont(f) 18 | 19 | ra := &ContinuousRange{ 20 | Min: 0.0, 21 | Max: 10.0, 22 | Domain: 256, 23 | } 24 | 25 | vf := FloatValueFormatter 26 | 27 | ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) 28 | testutil.AssertNotEmpty(t, ticks) 29 | testutil.AssertLen(t, ticks, 11) 30 | testutil.AssertEqual(t, 0.0, ticks[0].Value) 31 | testutil.AssertEqual(t, 10, ticks[len(ticks)-1].Value) 32 | } 33 | 34 | func TestGenerateContinuousTicksDescending(t *testing.T) { 35 | // replaced new assertions helper 36 | 37 | f, err := GetDefaultFont() 38 | testutil.AssertNil(t, err) 39 | 40 | r, err := PNG(1024, 1024) 41 | testutil.AssertNil(t, err) 42 | r.SetFont(f) 43 | 44 | ra := &ContinuousRange{ 45 | Min: 0.0, 46 | Max: 10.0, 47 | Domain: 256, 48 | Descending: true, 49 | } 50 | 51 | vf := FloatValueFormatter 52 | 53 | ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) 54 | testutil.AssertNotEmpty(t, ticks) 55 | testutil.AssertLen(t, ticks, 11) 56 | testutil.AssertEqual(t, 10.0, ticks[0].Value) 57 | testutil.AssertEqual(t, 9.0, ticks[1].Value) 58 | testutil.AssertEqual(t, 1.0, ticks[len(ticks)-2].Value) 59 | testutil.AssertEqual(t, 0.0, ticks[len(ticks)-1].Value) 60 | } 61 | -------------------------------------------------------------------------------- /time_series.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Interface Assertions. 9 | var ( 10 | _ Series = (*TimeSeries)(nil) 11 | _ FirstValuesProvider = (*TimeSeries)(nil) 12 | _ LastValuesProvider = (*TimeSeries)(nil) 13 | _ ValueFormatterProvider = (*TimeSeries)(nil) 14 | ) 15 | 16 | // TimeSeries is a line on a chart. 17 | type TimeSeries struct { 18 | Name string 19 | Style Style 20 | 21 | YAxis YAxisType 22 | 23 | XValues []time.Time 24 | YValues []float64 25 | } 26 | 27 | // GetName returns the name of the time series. 28 | func (ts TimeSeries) GetName() string { 29 | return ts.Name 30 | } 31 | 32 | // GetStyle returns the line style. 33 | func (ts TimeSeries) GetStyle() Style { 34 | return ts.Style 35 | } 36 | 37 | // Len returns the number of elements in the series. 38 | func (ts TimeSeries) Len() int { 39 | return len(ts.XValues) 40 | } 41 | 42 | // GetValues gets x, y values at a given index. 43 | func (ts TimeSeries) GetValues(index int) (x, y float64) { 44 | x = TimeToFloat64(ts.XValues[index]) 45 | y = ts.YValues[index] 46 | return 47 | } 48 | 49 | // GetFirstValues gets the first values. 50 | func (ts TimeSeries) GetFirstValues() (x, y float64) { 51 | x = TimeToFloat64(ts.XValues[0]) 52 | y = ts.YValues[0] 53 | return 54 | } 55 | 56 | // GetLastValues gets the last values. 57 | func (ts TimeSeries) GetLastValues() (x, y float64) { 58 | x = TimeToFloat64(ts.XValues[len(ts.XValues)-1]) 59 | y = ts.YValues[len(ts.YValues)-1] 60 | return 61 | } 62 | 63 | // GetValueFormatters returns value formatter defaults for the series. 64 | func (ts TimeSeries) GetValueFormatters() (x, y ValueFormatter) { 65 | x = TimeValueFormatter 66 | y = FloatValueFormatter 67 | return 68 | } 69 | 70 | // GetYAxis returns which YAxis the series draws on. 71 | func (ts TimeSeries) GetYAxis() YAxisType { 72 | return ts.YAxis 73 | } 74 | 75 | // Render renders the series. 76 | func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 77 | style := ts.Style.InheritFrom(defaults) 78 | Draw.LineSeries(r, canvasBox, xrange, yrange, style, ts) 79 | } 80 | 81 | // Validate validates the series. 82 | func (ts TimeSeries) Validate() error { 83 | if len(ts.XValues) == 0 { 84 | return fmt.Errorf("time series must have xvalues set") 85 | } 86 | 87 | if len(ts.YValues) == 0 { 88 | return fmt.Errorf("time series must have yvalues set") 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /time_series_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestTimeSeriesGetValue(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | ts := TimeSeries{ 14 | Name: "Test", 15 | XValues: []time.Time{ 16 | time.Now().AddDate(0, 0, -5), 17 | time.Now().AddDate(0, 0, -4), 18 | time.Now().AddDate(0, 0, -3), 19 | time.Now().AddDate(0, 0, -2), 20 | time.Now().AddDate(0, 0, -1), 21 | }, 22 | YValues: []float64{ 23 | 1.0, 2.0, 3.0, 4.0, 5.0, 24 | }, 25 | } 26 | 27 | x0, y0 := ts.GetValues(0) 28 | testutil.AssertNotZero(t, x0) 29 | testutil.AssertEqual(t, 1.0, y0) 30 | } 31 | 32 | func TestTimeSeriesValidate(t *testing.T) { 33 | // replaced new assertions helper 34 | 35 | cs := TimeSeries{ 36 | Name: "Test Series", 37 | XValues: []time.Time{ 38 | time.Now().AddDate(0, 0, -5), 39 | time.Now().AddDate(0, 0, -4), 40 | time.Now().AddDate(0, 0, -3), 41 | time.Now().AddDate(0, 0, -2), 42 | time.Now().AddDate(0, 0, -1), 43 | }, 44 | YValues: []float64{ 45 | 1.0, 2.0, 3.0, 4.0, 5.0, 46 | }, 47 | } 48 | testutil.AssertNil(t, cs.Validate()) 49 | 50 | cs = TimeSeries{ 51 | Name: "Test Series", 52 | XValues: []time.Time{ 53 | time.Now().AddDate(0, 0, -5), 54 | time.Now().AddDate(0, 0, -4), 55 | time.Now().AddDate(0, 0, -3), 56 | time.Now().AddDate(0, 0, -2), 57 | time.Now().AddDate(0, 0, -1), 58 | }, 59 | } 60 | testutil.AssertNotNil(t, cs.Validate()) 61 | 62 | cs = TimeSeries{ 63 | Name: "Test Series", 64 | YValues: []float64{ 65 | 1.0, 2.0, 3.0, 4.0, 5.0, 66 | }, 67 | } 68 | testutil.AssertNotNil(t, cs.Validate()) 69 | } 70 | -------------------------------------------------------------------------------- /times.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | ) 7 | 8 | // Assert types implement interfaces. 9 | var ( 10 | _ Sequence = (*Times)(nil) 11 | _ sort.Interface = (*Times)(nil) 12 | ) 13 | 14 | // Times are an array of times. 15 | // It wraps the array with methods that implement `seq.Provider`. 16 | type Times []time.Time 17 | 18 | // Array returns the times to an array. 19 | func (t Times) Array() []time.Time { 20 | return []time.Time(t) 21 | } 22 | 23 | // Len returns the length of the array. 24 | func (t Times) Len() int { 25 | return len(t) 26 | } 27 | 28 | // GetValue returns a value at an index as a time. 29 | func (t Times) GetValue(index int) float64 { 30 | return ToFloat64(t[index]) 31 | } 32 | 33 | // Swap implements sort.Interface. 34 | func (t Times) Swap(i, j int) { 35 | t[i], t[j] = t[j], t[i] 36 | } 37 | 38 | // Less implements sort.Interface. 39 | func (t Times) Less(i, j int) bool { 40 | return t[i].Before(t[j]) 41 | } 42 | 43 | // ToFloat64 returns a float64 representation of a time. 44 | func ToFloat64(t time.Time) float64 { 45 | return float64(t.UnixNano()) 46 | } 47 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // Value is a chart value. 4 | type Value struct { 5 | Style Style 6 | Label string 7 | Value float64 8 | } 9 | 10 | // Values is an array of Value. 11 | type Values []Value 12 | 13 | // Values returns the values. 14 | func (vs Values) Values() []float64 { 15 | values := make([]float64, len(vs)) 16 | for index, v := range vs { 17 | values[index] = v.Value 18 | } 19 | return values 20 | } 21 | 22 | // ValuesNormalized returns normalized values. 23 | func (vs Values) ValuesNormalized() []float64 { 24 | return Normalize(vs.Values()...) 25 | } 26 | 27 | // Normalize returns the values normalized. 28 | func (vs Values) Normalize() []Value { 29 | var output []Value 30 | var total float64 31 | 32 | for _, v := range vs { 33 | total += v.Value 34 | } 35 | 36 | for _, v := range vs { 37 | if v.Value > 0 { 38 | output = append(output, Value{ 39 | Style: v.Style, 40 | Label: v.Label, 41 | Value: RoundDown(v.Value/total, 0.0001), 42 | }) 43 | } 44 | } 45 | return output 46 | } 47 | 48 | // Value2 is a two axis value. 49 | type Value2 struct { 50 | Style Style 51 | Label string 52 | XValue, YValue float64 53 | } 54 | -------------------------------------------------------------------------------- /value_formatter.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // ValueFormatter is a function that takes a value and produces a string. 10 | type ValueFormatter func(v interface{}) string 11 | 12 | // TimeValueFormatter is a ValueFormatter for timestamps. 13 | func TimeValueFormatter(v interface{}) string { 14 | return formatTime(v, DefaultDateFormat) 15 | } 16 | 17 | // TimeHourValueFormatter is a ValueFormatter for timestamps. 18 | func TimeHourValueFormatter(v interface{}) string { 19 | return formatTime(v, DefaultDateHourFormat) 20 | } 21 | 22 | // TimeMinuteValueFormatter is a ValueFormatter for timestamps. 23 | func TimeMinuteValueFormatter(v interface{}) string { 24 | return formatTime(v, DefaultDateMinuteFormat) 25 | } 26 | 27 | // TimeDateValueFormatter is a ValueFormatter for timestamps. 28 | func TimeDateValueFormatter(v interface{}) string { 29 | return formatTime(v, "2006-01-02") 30 | } 31 | 32 | // TimeValueFormatterWithFormat returns a time formatter with a given format. 33 | func TimeValueFormatterWithFormat(format string) ValueFormatter { 34 | return func(v interface{}) string { 35 | return formatTime(v, format) 36 | } 37 | } 38 | 39 | // TimeValueFormatterWithFormat is a ValueFormatter for timestamps with a given format. 40 | func formatTime(v interface{}, dateFormat string) string { 41 | if typed, isTyped := v.(time.Time); isTyped { 42 | return typed.Format(dateFormat) 43 | } 44 | if typed, isTyped := v.(int64); isTyped { 45 | return time.Unix(0, typed).Format(dateFormat) 46 | } 47 | if typed, isTyped := v.(float64); isTyped { 48 | return time.Unix(0, int64(typed)).Format(dateFormat) 49 | } 50 | return "" 51 | } 52 | 53 | // IntValueFormatter is a ValueFormatter for float64. 54 | func IntValueFormatter(v interface{}) string { 55 | switch v.(type) { 56 | case int: 57 | return strconv.Itoa(v.(int)) 58 | case int64: 59 | return strconv.FormatInt(v.(int64), 10) 60 | case float32: 61 | return strconv.FormatInt(int64(v.(float32)), 10) 62 | case float64: 63 | return strconv.FormatInt(int64(v.(float64)), 10) 64 | default: 65 | return "" 66 | } 67 | } 68 | 69 | // FloatValueFormatter is a ValueFormatter for float64. 70 | func FloatValueFormatter(v interface{}) string { 71 | return FloatValueFormatterWithFormat(v, DefaultFloatFormat) 72 | } 73 | 74 | // PercentValueFormatter is a formatter for percent values. 75 | // NOTE: it normalizes the values, i.e. multiplies by 100.0. 76 | func PercentValueFormatter(v interface{}) string { 77 | if typed, isTyped := v.(float64); isTyped { 78 | return FloatValueFormatterWithFormat(typed*100.0, DefaultPercentValueFormat) 79 | } 80 | return "" 81 | } 82 | 83 | // FloatValueFormatterWithFormat is a ValueFormatter for float64 with a given format. 84 | func FloatValueFormatterWithFormat(v interface{}, floatFormat string) string { 85 | if typed, isTyped := v.(int); isTyped { 86 | return fmt.Sprintf(floatFormat, float64(typed)) 87 | } 88 | if typed, isTyped := v.(int64); isTyped { 89 | return fmt.Sprintf(floatFormat, float64(typed)) 90 | } 91 | if typed, isTyped := v.(float32); isTyped { 92 | return fmt.Sprintf(floatFormat, typed) 93 | } 94 | if typed, isTyped := v.(float64); isTyped { 95 | return fmt.Sprintf(floatFormat, typed) 96 | } 97 | return "" 98 | } 99 | 100 | // KValueFormatter is a formatter for K values. 101 | func KValueFormatter(k float64, vf ValueFormatter) ValueFormatter { 102 | return func(v interface{}) string { 103 | return fmt.Sprintf("%0.0fσ %s", k, vf(v)) 104 | } 105 | } 106 | 107 | // FloatValueFormatter is a ValueFormatter for float64, exponential notation, e.g. 1.52e+08. 108 | func ExponentialValueFormatter(v interface{}) string { 109 | return FloatValueFormatterWithFormat(v, "%.2e") 110 | } 111 | -------------------------------------------------------------------------------- /value_formatter_provider.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | // ValueFormatterProvider is a series that has custom formatters. 4 | type ValueFormatterProvider interface { 5 | GetValueFormatters() (x, y ValueFormatter) 6 | } 7 | -------------------------------------------------------------------------------- /value_formatter_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/wcharczuk/go-chart/v2/testutil" 8 | ) 9 | 10 | func TestTimeValueFormatterWithFormat(t *testing.T) { 11 | // replaced new assertions helper 12 | 13 | d := time.Now() 14 | di := TimeToFloat64(d) 15 | df := float64(di) 16 | 17 | s := formatTime(d, DefaultDateFormat) 18 | si := formatTime(di, DefaultDateFormat) 19 | sf := formatTime(df, DefaultDateFormat) 20 | testutil.AssertEqual(t, s, si) 21 | testutil.AssertEqual(t, s, sf) 22 | 23 | sd := TimeValueFormatter(d) 24 | sdi := TimeValueFormatter(di) 25 | sdf := TimeValueFormatter(df) 26 | testutil.AssertEqual(t, s, sd) 27 | testutil.AssertEqual(t, s, sdi) 28 | testutil.AssertEqual(t, s, sdf) 29 | } 30 | 31 | func TestFloatValueFormatter(t *testing.T) { 32 | // replaced new assertions helper 33 | testutil.AssertEqual(t, "1234.00", FloatValueFormatter(1234.00)) 34 | } 35 | 36 | func TestFloatValueFormatterWithFloat32Input(t *testing.T) { 37 | // replaced new assertions helper 38 | testutil.AssertEqual(t, "1234.00", FloatValueFormatter(float32(1234.00))) 39 | } 40 | 41 | func TestFloatValueFormatterWithIntegerInput(t *testing.T) { 42 | // replaced new assertions helper 43 | testutil.AssertEqual(t, "1234.00", FloatValueFormatter(1234)) 44 | } 45 | 46 | func TestFloatValueFormatterWithInt64Input(t *testing.T) { 47 | // replaced new assertions helper 48 | testutil.AssertEqual(t, "1234.00", FloatValueFormatter(int64(1234))) 49 | } 50 | 51 | func TestFloatValueFormatterWithFormat(t *testing.T) { 52 | // replaced new assertions helper 53 | 54 | v := 123.456 55 | sv := FloatValueFormatterWithFormat(v, "%.3f") 56 | testutil.AssertEqual(t, "123.456", sv) 57 | testutil.AssertEqual(t, "123.000", FloatValueFormatterWithFormat(123, "%.3f")) 58 | } 59 | 60 | func TestExponentialValueFormatter(t *testing.T) { 61 | testutil.AssertEqual(t, "1.23e+02", ExponentialValueFormatter(123.456)) 62 | testutil.AssertEqual(t, "1.24e+07", ExponentialValueFormatter(12421243.424)) 63 | testutil.AssertEqual(t, "4.50e-01", ExponentialValueFormatter(0.45)) 64 | } 65 | -------------------------------------------------------------------------------- /value_provider.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "github.com/wcharczuk/go-chart/v2/drawing" 4 | 5 | // ValuesProvider is a type that produces values. 6 | type ValuesProvider interface { 7 | Len() int 8 | GetValues(index int) (float64, float64) 9 | } 10 | 11 | // BoundedValuesProvider allows series to return a range. 12 | type BoundedValuesProvider interface { 13 | Len() int 14 | GetBoundedValues(index int) (x, y1, y2 float64) 15 | } 16 | 17 | // FirstValuesProvider is a special type of value provider that can return it's (potentially computed) first value. 18 | type FirstValuesProvider interface { 19 | GetFirstValues() (x, y float64) 20 | } 21 | 22 | // LastValuesProvider is a special type of value provider that can return it's (potentially computed) last value. 23 | type LastValuesProvider interface { 24 | GetLastValues() (x, y float64) 25 | } 26 | 27 | // BoundedLastValuesProvider is a special type of value provider that can return it's (potentially computed) bounded last value. 28 | type BoundedLastValuesProvider interface { 29 | GetBoundedLastValues() (x, y1, y2 float64) 30 | } 31 | 32 | // FullValuesProvider is an interface that combines `ValuesProvider` and `LastValuesProvider` 33 | type FullValuesProvider interface { 34 | ValuesProvider 35 | LastValuesProvider 36 | } 37 | 38 | // FullBoundedValuesProvider is an interface that combines `BoundedValuesProvider` and `BoundedLastValuesProvider` 39 | type FullBoundedValuesProvider interface { 40 | BoundedValuesProvider 41 | BoundedLastValuesProvider 42 | } 43 | 44 | // SizeProvider is a provider for integer size. 45 | type SizeProvider func(xrange, yrange Range, index int, x, y float64) float64 46 | 47 | // ColorProvider is a general provider for color ranges based on values. 48 | type ColorProvider func(v, vmin, vmax float64) drawing.Color 49 | 50 | // DotColorProvider is a provider for dot color. 51 | type DotColorProvider func(xrange, yrange Range, index int, x, y float64) drawing.Color 52 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestValuesValues(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | vs := []Value{ 13 | {Value: 10, Label: "Blue"}, 14 | {Value: 9, Label: "Green"}, 15 | {Value: 8, Label: "Gray"}, 16 | {Value: 7, Label: "Orange"}, 17 | {Value: 6, Label: "HEANG"}, 18 | {Value: 5, Label: "??"}, 19 | {Value: 2, Label: "!!"}, 20 | } 21 | 22 | values := Values(vs).Values() 23 | testutil.AssertLen(t, values, 7) 24 | testutil.AssertEqual(t, 10, values[0]) 25 | testutil.AssertEqual(t, 9, values[1]) 26 | testutil.AssertEqual(t, 8, values[2]) 27 | testutil.AssertEqual(t, 7, values[3]) 28 | testutil.AssertEqual(t, 6, values[4]) 29 | testutil.AssertEqual(t, 5, values[5]) 30 | testutil.AssertEqual(t, 2, values[6]) 31 | } 32 | 33 | func TestValuesValuesNormalized(t *testing.T) { 34 | // replaced new assertions helper 35 | 36 | vs := []Value{ 37 | {Value: 10, Label: "Blue"}, 38 | {Value: 9, Label: "Green"}, 39 | {Value: 8, Label: "Gray"}, 40 | {Value: 7, Label: "Orange"}, 41 | {Value: 6, Label: "HEANG"}, 42 | {Value: 5, Label: "??"}, 43 | {Value: 2, Label: "!!"}, 44 | } 45 | 46 | values := Values(vs).ValuesNormalized() 47 | testutil.AssertLen(t, values, 7) 48 | testutil.AssertEqual(t, 0.2127, values[0]) 49 | testutil.AssertEqual(t, 0.0425, values[6]) 50 | } 51 | 52 | func TestValuesNormalize(t *testing.T) { 53 | // replaced new assertions helper 54 | 55 | vs := []Value{ 56 | {Value: 10, Label: "Blue"}, 57 | {Value: 9, Label: "Green"}, 58 | {Value: 8, Label: "Gray"}, 59 | {Value: 7, Label: "Orange"}, 60 | {Value: 6, Label: "HEANG"}, 61 | {Value: 5, Label: "??"}, 62 | {Value: 2, Label: "!!"}, 63 | } 64 | 65 | values := Values(vs).Normalize() 66 | testutil.AssertLen(t, values, 7) 67 | testutil.AssertEqual(t, 0.2127, values[0].Value) 68 | testutil.AssertEqual(t, 0.0425, values[6].Value) 69 | } 70 | -------------------------------------------------------------------------------- /vector_renderer_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/wcharczuk/go-chart/v2/drawing" 10 | "github.com/wcharczuk/go-chart/v2/testutil" 11 | ) 12 | 13 | func TestVectorRendererPath(t *testing.T) { 14 | // replaced new assertions helper 15 | 16 | vr, err := SVG(100, 100) 17 | testutil.AssertNil(t, err) 18 | 19 | typed, isTyped := vr.(*vectorRenderer) 20 | testutil.AssertTrue(t, isTyped) 21 | 22 | typed.MoveTo(0, 0) 23 | typed.LineTo(100, 100) 24 | typed.LineTo(0, 100) 25 | typed.Close() 26 | typed.FillStroke() 27 | 28 | buffer := bytes.NewBuffer([]byte{}) 29 | err = typed.Save(buffer) 30 | testutil.AssertNil(t, err) 31 | 32 | raw := string(buffer.Bytes()) 33 | 34 | testutil.AssertTrue(t, strings.HasPrefix(raw, "")) 36 | } 37 | 38 | func TestVectorRendererMeasureText(t *testing.T) { 39 | // replaced new assertions helper 40 | 41 | f, err := GetDefaultFont() 42 | testutil.AssertNil(t, err) 43 | 44 | vr, err := SVG(100, 100) 45 | testutil.AssertNil(t, err) 46 | 47 | vr.SetDPI(DefaultDPI) 48 | vr.SetFont(f) 49 | vr.SetFontSize(12.0) 50 | 51 | tb := vr.MeasureText("Ljp") 52 | testutil.AssertEqual(t, 21, tb.Width()) 53 | testutil.AssertEqual(t, 15, tb.Height()) 54 | } 55 | 56 | func TestCanvasStyleSVG(t *testing.T) { 57 | // replaced new assertions helper 58 | 59 | f, err := GetDefaultFont() 60 | testutil.AssertNil(t, err) 61 | 62 | set := Style{ 63 | StrokeColor: drawing.ColorWhite, 64 | StrokeWidth: 5.0, 65 | FillColor: drawing.ColorWhite, 66 | FontColor: drawing.ColorWhite, 67 | Font: f, 68 | Padding: DefaultBackgroundPadding, 69 | } 70 | 71 | canvas := &canvas{dpi: DefaultDPI} 72 | 73 | svgString := canvas.styleAsSVG(set) 74 | testutil.AssertNotEmpty(t, svgString) 75 | testutil.AssertTrue(t, strings.HasPrefix(svgString, "style=\"")) 76 | testutil.AssertTrue(t, strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)")) 77 | testutil.AssertTrue(t, strings.Contains(svgString, "stroke-width:5")) 78 | testutil.AssertTrue(t, strings.Contains(svgString, "fill:rgba(255,255,255,1.0)")) 79 | testutil.AssertTrue(t, strings.HasSuffix(svgString, "\"")) 80 | } 81 | 82 | func TestCanvasClassSVG(t *testing.T) { 83 | set := Style{ 84 | ClassName: "test-class", 85 | } 86 | 87 | canvas := &canvas{dpi: DefaultDPI} 88 | 89 | testutil.AssertEqual(t, "class=\"test-class\"", canvas.styleAsSVG(set)) 90 | } 91 | 92 | func TestCanvasCustomInlineStylesheet(t *testing.T) { 93 | b := strings.Builder{} 94 | 95 | canvas := &canvas{ 96 | w: &b, 97 | css: ".background { fill: red }", 98 | } 99 | 100 | canvas.Start(200, 200) 101 | 102 | testutil.AssertContains(t, b.String(), fmt.Sprintf(``, canvas.css)) 103 | } 104 | 105 | func TestCanvasCustomInlineStylesheetWithNonce(t *testing.T) { 106 | b := strings.Builder{} 107 | 108 | canvas := &canvas{ 109 | w: &b, 110 | css: ".background { fill: red }", 111 | nonce: "RAND0MSTRING", 112 | } 113 | 114 | canvas.Start(200, 200) 115 | 116 | testutil.AssertContains(t, b.String(), fmt.Sprintf(``, canvas.nonce, canvas.css)) 117 | } 118 | -------------------------------------------------------------------------------- /xaxis_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestXAxisGetTicks(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | r, err := PNG(1024, 1024) 13 | testutil.AssertNil(t, err) 14 | 15 | f, err := GetDefaultFont() 16 | testutil.AssertNil(t, err) 17 | 18 | xa := XAxis{} 19 | xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 20 | styleDefaults := Style{ 21 | Font: f, 22 | FontSize: 10.0, 23 | } 24 | vf := FloatValueFormatter 25 | ticks := xa.GetTicks(r, xr, styleDefaults, vf) 26 | testutil.AssertLen(t, ticks, 16) 27 | } 28 | 29 | func TestXAxisGetTicksWithUserDefaults(t *testing.T) { 30 | // replaced new assertions helper 31 | 32 | r, err := PNG(1024, 1024) 33 | testutil.AssertNil(t, err) 34 | 35 | f, err := GetDefaultFont() 36 | testutil.AssertNil(t, err) 37 | 38 | xa := XAxis{ 39 | Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, 40 | } 41 | xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 42 | styleDefaults := Style{ 43 | Font: f, 44 | FontSize: 10.0, 45 | } 46 | vf := FloatValueFormatter 47 | ticks := xa.GetTicks(r, xr, styleDefaults, vf) 48 | testutil.AssertLen(t, ticks, 1) 49 | } 50 | 51 | func TestXAxisMeasure(t *testing.T) { 52 | // replaced new assertions helper 53 | 54 | f, err := GetDefaultFont() 55 | testutil.AssertNil(t, err) 56 | style := Style{ 57 | Font: f, 58 | FontSize: 10.0, 59 | } 60 | r, err := PNG(100, 100) 61 | testutil.AssertNil(t, err) 62 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 63 | xa := XAxis{} 64 | xab := xa.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 65 | testutil.AssertEqual(t, 122, xab.Width()) 66 | testutil.AssertEqual(t, 21, xab.Height()) 67 | } 68 | -------------------------------------------------------------------------------- /yaxis_test.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/wcharczuk/go-chart/v2/testutil" 7 | ) 8 | 9 | func TestYAxisGetTicks(t *testing.T) { 10 | // replaced new assertions helper 11 | 12 | r, err := PNG(1024, 1024) 13 | testutil.AssertNil(t, err) 14 | 15 | f, err := GetDefaultFont() 16 | testutil.AssertNil(t, err) 17 | 18 | ya := YAxis{} 19 | yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 20 | styleDefaults := Style{ 21 | Font: f, 22 | FontSize: 10.0, 23 | } 24 | vf := FloatValueFormatter 25 | ticks := ya.GetTicks(r, yr, styleDefaults, vf) 26 | testutil.AssertLen(t, ticks, 32) 27 | } 28 | 29 | func TestYAxisGetTicksWithUserDefaults(t *testing.T) { 30 | // replaced new assertions helper 31 | 32 | r, err := PNG(1024, 1024) 33 | testutil.AssertNil(t, err) 34 | 35 | f, err := GetDefaultFont() 36 | testutil.AssertNil(t, err) 37 | 38 | ya := YAxis{ 39 | Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, 40 | } 41 | yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 42 | styleDefaults := Style{ 43 | Font: f, 44 | FontSize: 10.0, 45 | } 46 | vf := FloatValueFormatter 47 | ticks := ya.GetTicks(r, yr, styleDefaults, vf) 48 | testutil.AssertLen(t, ticks, 1) 49 | } 50 | 51 | func TestYAxisMeasure(t *testing.T) { 52 | // replaced new assertions helper 53 | 54 | f, err := GetDefaultFont() 55 | testutil.AssertNil(t, err) 56 | style := Style{ 57 | Font: f, 58 | FontSize: 10.0, 59 | } 60 | r, err := PNG(100, 100) 61 | testutil.AssertNil(t, err) 62 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 63 | ya := YAxis{} 64 | yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 65 | testutil.AssertEqual(t, 32, yab.Width()) 66 | testutil.AssertEqual(t, 110, yab.Height()) 67 | } 68 | 69 | func TestYAxisSecondaryMeasure(t *testing.T) { 70 | // replaced new assertions helper 71 | 72 | f, err := GetDefaultFont() 73 | testutil.AssertNil(t, err) 74 | style := Style{ 75 | Font: f, 76 | FontSize: 10.0, 77 | } 78 | r, err := PNG(100, 100) 79 | testutil.AssertNil(t, err) 80 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 81 | ya := YAxis{AxisType: YAxisSecondary} 82 | yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 83 | testutil.AssertEqual(t, 32, yab.Width()) 84 | testutil.AssertEqual(t, 110, yab.Height()) 85 | } 86 | --------------------------------------------------------------------------------