├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── tests-main.yml │ ├── tests-pr.yml │ └── tests-workflow.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── alias.go ├── assert_test.go ├── assets ├── chart-bar.png ├── chart-doughnut.png ├── chart-heat_map.png ├── chart-horizontal-bar.png ├── chart-line.png ├── chart-pie.png ├── chart-radar.png ├── chart-scatter.png ├── chart-table.png └── themes.png ├── axis.go ├── axis_test.go ├── bar_chart.go ├── bar_chart_test.go ├── benchmark_test.go ├── chart_option.go ├── chart_option_test.go ├── chartdraw ├── _colors │ └── colors_extended.txt ├── annotation_series.go ├── annotation_series_test.go ├── array.go ├── axis.go ├── bar_chart.go ├── bar_chart_test.go ├── benchmark_test.go ├── bollinger_band_series.go ├── bollinger_band_series_test.go ├── box.go ├── box_test.go ├── chart.go ├── chart_test.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 │ ├── curve.go │ ├── curve_test.go │ ├── dasher.go │ ├── dasher_test.go │ ├── demux_flattener.go │ ├── demux_flattener_test.go │ ├── drawing.go │ ├── flattener.go │ ├── flattener_test.go │ ├── free_type_path.go │ ├── graphic_context.go │ ├── image_filter.go │ ├── line.go │ ├── line_test.go │ ├── matrix.go │ ├── matrix_test.go │ ├── painter.go │ ├── painter_draw_test.go │ ├── path.go │ ├── path_test.go │ ├── raster_graphic_context.go │ ├── raster_graphic_context_test.go │ ├── stack_graphic_context.go │ ├── stack_graphic_context_test.go │ ├── stroker.go │ ├── stroker_test.go │ ├── text.go │ ├── text_test.go │ ├── transformer.go │ ├── util.go │ └── util_test.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 │ ├── 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 ├── first_value_annotation.go ├── first_value_annotation_test.go ├── font.go ├── 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 ├── macd_series.go ├── macd_series_test.go ├── mathutil.go ├── mathutil_test.go ├── matrix │ ├── matrix.go │ ├── matrix_test.go │ ├── regression.go │ ├── regression_test.go │ ├── util.go │ ├── vector.go │ └── vector_test.go ├── min_max_series.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 ├── raster_renderer_test.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 ├── style.go ├── style_test.go ├── text.go ├── text_test.go ├── tick.go ├── tick_test.go ├── time_series.go ├── time_series_test.go ├── times.go ├── timeutil.go ├── timeutil_test.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 ├── charts.go ├── color.go ├── color_test.go ├── doughnut_chart.go ├── doughnut_chart_test.go ├── echarts.go ├── echarts_test.go ├── examples ├── 1-Painter │ ├── bar_chart-1-basic │ │ └── main.go │ ├── bar_chart-2-size_margin │ │ └── main.go │ ├── bar_chart-3-label_position-round_caps │ │ └── main.go │ ├── bar_chart-4-mark │ │ └── main.go │ ├── bar_chart-5-stacked │ │ └── main.go │ ├── doughnut_chart-1-basic │ │ └── main.go │ ├── doughnut_chart-2-styles │ │ └── main.go │ ├── funnel_chart-1-basic │ │ └── main.go │ ├── heat_map-1-basic │ │ └── main.go │ ├── horizontal_bar_chart-1-basic │ │ └── main.go │ ├── horizontal_bar_chart-2-size_margin │ │ └── main.go │ ├── horizontal_bar_chart-3-mark │ │ └── main.go │ ├── horizontal_bar_chart-4-stacked │ │ └── main.go │ ├── line_chart-1-basic │ │ └── main.go │ ├── line_chart-2-symbols │ │ └── main.go │ ├── line_chart-3-smooth │ │ └── main.go │ ├── line_chart-4-mark │ │ └── main.go │ ├── line_chart-5-area │ │ └── main.go │ ├── line_chart-6-stacked │ │ └── main.go │ ├── line_chart-7-boundary_gap │ │ └── main.go │ ├── line_chart-8-dual_y_axis │ │ └── main.go │ ├── line_chart-9-custom │ │ └── main.go │ ├── multiple_charts-1 │ │ └── main.go │ ├── multiple_charts-2 │ │ └── main.go │ ├── pie_chart-1-basic │ │ └── main.go │ ├── pie_chart-2-series_radius │ │ └── main.go │ ├── pie_chart-3-gap │ │ └── main.go │ ├── radar_chart-1-basic │ │ └── main.go │ ├── scatter_chart-1-basic │ │ └── main.go │ ├── scatter_chart-2-symbols │ │ └── main.go │ ├── scatter_chart-3-dense_data │ │ └── main.go │ └── table-1 │ │ └── main.go ├── 2-OptionFunc │ ├── bar_chart-1-basic │ │ └── main.go │ ├── chinese │ │ └── main.go │ ├── doughnut_chart-1 │ │ └── main.go │ ├── funnel_chart-1-basic │ │ └── main.go │ ├── horizontal_bar_chart-1-basic │ │ └── main.go │ ├── line_chart-1-basic │ │ └── main.go │ ├── line_chart-2-dense_data │ │ └── main.go │ ├── line_chart-3-area │ │ └── main.go │ ├── multiple_charts-1 │ │ └── main.go │ ├── multiple_charts-2 │ │ └── main.go │ ├── pie_chart-1 │ │ └── main.go │ ├── radar_chart-1-basic │ │ └── main.go │ ├── scatter_chart-1-basic │ │ └── main.go │ ├── table-1 │ │ └── main.go │ └── web-1 │ │ └── main.go ├── README.md └── demo │ └── themes │ └── main.go ├── font.go ├── font_test.go ├── funnel_chart.go ├── funnel_chart_test.go ├── go.mod ├── go.sum ├── heat_map.go ├── heat_map_test.go ├── horizontal_bar_chart.go ├── horizontal_bar_chart_test.go ├── legend.go ├── legend_test.go ├── line_chart.go ├── line_chart_test.go ├── mark_line.go ├── mark_line_test.go ├── mark_point.go ├── mark_point_test.go ├── painter.go ├── painter_test.go ├── pie_chart.go ├── pie_chart_test.go ├── radar_chart.go ├── radar_chart_test.go ├── range.go ├── range_test.go ├── scatter_chart.go ├── scatter_chart_test.go ├── series.go ├── series_label.go ├── series_label_test.go ├── series_test.go ├── table.go ├── table_test.go ├── theme.go ├── theme_test.go ├── title.go ├── title_test.go ├── trend_line.go ├── trend_line_test.go ├── util.go ├── util_test.go ├── xaxis.go ├── xaxis_test.go ├── yaxis.go └── yaxis_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Reproduction Chart Code** 14 | Provide executable Go code to produce the chart which demonstrates the issue. 15 | 16 | **Screenshots** 17 | Attach chart PNG. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | jobs: 9 | analyze: 10 | name: Analyze (${{ matrix.language }}) 11 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 12 | permissions: 13 | security-events: write 14 | packages: read 15 | actions: read 16 | contents: read 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | cache: false 31 | go-version-file: go.mod 32 | if: ${{ matrix.language == 'go' }} 33 | 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | timeout-minutes: 10 39 | 40 | - name: Autobuild (${{ matrix.language }}) 41 | uses: github/codeql-action/autobuild@v3 42 | timeout-minutes: 10 43 | 44 | - name: Perform CodeQL Analysis (${{ matrix.language }}) 45 | uses: github/codeql-action/analyze@v3 46 | with: 47 | category: "/language:${{matrix.language}}" 48 | timeout-minutes: 10 49 | -------------------------------------------------------------------------------- /.github/workflows/tests-main.yml: -------------------------------------------------------------------------------- 1 | name: Tests - Main Push 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | call-reusable: 9 | uses: ./.github/workflows/tests-workflow.yml 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/tests-pr.yml: -------------------------------------------------------------------------------- 1 | name: Tests - Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | call-reusable: 8 | uses: ./.github/workflows/tests-workflow.yml 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/tests-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | lint-and-tidy: 8 | name: Verify Linting 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: "1.24" 17 | 18 | - name: Set up golangci-lint 19 | run: | 20 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v1.64.8/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8 21 | 22 | - name: Run lint check 23 | run: make lint 24 | 25 | - name: Run tidy check 26 | run: | 27 | go mod tidy 28 | # Fail if go.mod or go.sum changed 29 | git diff --exit-code go.mod go.sum 30 | 31 | test: 32 | name: Verify Unit Tests 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | go: 37 | - '1.24' 38 | - '1.23' 39 | - '1.22' 40 | - '1.21' 41 | - '1.20' 42 | - '1.19' 43 | - '1.18' 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Set up Go ${{ matrix.go }} 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version: ${{ matrix.go }} 51 | 52 | - name: Run unit tests 53 | run: make test 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | *.png 17 | *.svg 18 | tmp 19 | NotoSansSC.ttf 20 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 William Charczuk 4 | Copyright (c) 2021 Tree Xie 5 | Copyright (c) 2024 Mike Jensen 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO111MODULE = on 2 | 3 | .PHONY: default test test-cover bench lint 4 | 5 | 6 | test: 7 | go test -race -cover ./... 8 | 9 | test-cover: 10 | go test -race -coverprofile=test.out ./... && go tool cover --html=test.out 11 | 12 | bench: 13 | $(eval CORES_HALF := $(shell expr `getconf _NPROCESSORS_ONLN` / 2)) 14 | go test -parallel=$(CORES_HALF) --benchmem -benchtime=20s -bench='Benchmark.*Render' -run='^$$' 15 | 16 | lint: 17 | golangci-lint run --timeout=600s --enable=asasalint,asciicheck,bidichk,containedctx,contextcheck,decorder,durationcheck,errorlint,exptostd,fatcontext,forbidigo,gocheckcompilerdirectives,gochecksumtype,goconst,gofmt,goimports,gosmopolitan,grouper,iface,importas,mirror,misspell,nilerr,nilnil,perfsprint,prealloc,reassign,recvcheck,sloglint,testifylint,unconvert,wastedassign,whitespace 18 | 19 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func assertEqualSVG(t *testing.T, expected string, actual []byte) { 13 | t.Helper() 14 | 15 | actualStr := string(actual) 16 | if expected != actualStr { 17 | actualFile, err1 := writeTempFile(actual, t.Name()+"-actual", "svg") 18 | 19 | if expected == "" { 20 | t.Errorf("SVG written to %s", actualFile) 21 | } else { 22 | expectedFile, err2 := writeTempFile([]byte(expected), t.Name()+"-expected", "svg") 23 | t.Errorf("SVG content does not match. Expected file: %s, Actual file: %s", 24 | expectedFile, actualFile) 25 | require.NoError(t, err2) 26 | } 27 | require.NoError(t, err1) 28 | } 29 | } 30 | 31 | func writeTempFile(content []byte, prefix, extension string) (string, error) { 32 | tmpFile, err := os.CreateTemp("", strings.ReplaceAll(prefix, string(os.PathSeparator), ".")+"-*."+extension) 33 | if err != nil { 34 | return "", err 35 | } 36 | defer tmpFile.Close() 37 | 38 | if _, err := tmpFile.Write(content); err != nil { 39 | return "", err 40 | } 41 | 42 | return filepath.Abs(tmpFile.Name()) 43 | } 44 | -------------------------------------------------------------------------------- /assets/chart-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-bar.png -------------------------------------------------------------------------------- /assets/chart-doughnut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-doughnut.png -------------------------------------------------------------------------------- /assets/chart-heat_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-heat_map.png -------------------------------------------------------------------------------- /assets/chart-horizontal-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-horizontal-bar.png -------------------------------------------------------------------------------- /assets/chart-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-line.png -------------------------------------------------------------------------------- /assets/chart-pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-pie.png -------------------------------------------------------------------------------- /assets/chart-radar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-radar.png -------------------------------------------------------------------------------- /assets/chart-scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-scatter.png -------------------------------------------------------------------------------- /assets/chart-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/chart-table.png -------------------------------------------------------------------------------- /assets/themes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/assets/themes.png -------------------------------------------------------------------------------- /chartdraw/array.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 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 | -------------------------------------------------------------------------------- /chartdraw/axis.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | // GetGridLines 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 | -------------------------------------------------------------------------------- /chartdraw/bollinger_band_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBollingerBandSeries(t *testing.T) { 11 | t.Parallel() 12 | 13 | s1 := mockValuesProvider{ 14 | X: LinearRange(1.0, 100.0), 15 | Y: RandomValuesWithMax(100, 1024), 16 | } 17 | 18 | bbs := &BollingerBandsSeries{ 19 | InnerSeries: s1, 20 | } 21 | 22 | xvalues := make([]float64, 100) 23 | y1values := make([]float64, 100) 24 | y2values := make([]float64, 100) 25 | 26 | for x := 0; x < 100; x++ { 27 | xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x) 28 | } 29 | 30 | for x := bbs.GetPeriod(); x < 100; x++ { 31 | assert.Greater(t, y1values[x], y2values[x]) 32 | } 33 | } 34 | 35 | func TestBollingerBandLastValue(t *testing.T) { 36 | t.Parallel() 37 | 38 | s1 := mockValuesProvider{ 39 | X: LinearRange(1.0, 100.0), 40 | Y: LinearRange(1.0, 100.0), 41 | } 42 | 43 | bbs := &BollingerBandsSeries{ 44 | InnerSeries: s1, 45 | } 46 | 47 | x, y1, y2 := bbs.GetBoundedLastValues() 48 | assert.InDelta(t, 100.0, x, 0) 49 | assert.InDelta(t, float64(101), math.Floor(y1), 0) 50 | assert.InDelta(t, float64(83), math.Floor(y2), 0) 51 | } 52 | -------------------------------------------------------------------------------- /chartdraw/concat_series.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | length := typed.Len() 24 | if index < cursor+length { 25 | x, y = typed.GetValues(index - cursor) //FENCEPOSTS. 26 | return 27 | } 28 | cursor += length 29 | } 30 | } 31 | return 32 | } 33 | 34 | // Validate validates the series. 35 | func (cs ConcatSeries) Validate() error { 36 | for _, s := range cs { 37 | if err := s.Validate(); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /chartdraw/concat_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestConcatSeries(t *testing.T) { 10 | t.Parallel() 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 | assert.Equal(t, 30, cs.Len()) 29 | 30 | x0, y0 := cs.GetValue(0) 31 | assert.InDelta(t, 1.0, x0, 0) 32 | assert.InDelta(t, 1.0, y0, 0) 33 | 34 | xm, ym := cs.GetValue(19) 35 | assert.InDelta(t, 20.0, xm, 0) 36 | assert.InDelta(t, 1.0, ym, 0) 37 | 38 | xn, yn := cs.GetValue(29) 39 | assert.InDelta(t, 30.0, xn, 0) 40 | assert.InDelta(t, 10.0, yn, 0) 41 | } 42 | -------------------------------------------------------------------------------- /chartdraw/continuous_range.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/continuous_range_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRangeTranslate(t *testing.T) { 10 | t.Parallel() 11 | 12 | values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} 13 | r := ContinuousRange{Domain: 1000} 14 | r.Min, r.Max = MinMax(values...) 15 | 16 | // delta = ~7.0 17 | // value = ~5.0 18 | // domain = ~1000 19 | // 5/8 * 1000 ~= 20 | assert.Equal(t, 0, r.Translate(1.0)) 21 | assert.Equal(t, 1000, r.Translate(8.0)) 22 | assert.Equal(t, 572, r.Translate(5.0)) 23 | } 24 | -------------------------------------------------------------------------------- /chartdraw/continuous_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestContinuousSeries(t *testing.T) { 12 | t.Parallel() 13 | 14 | cs := ContinuousSeries{ 15 | Name: "Test Series", 16 | XValues: LinearRange(1.0, 10.0), 17 | YValues: LinearRange(1.0, 10.0), 18 | } 19 | 20 | assert.Equal(t, "Test Series", cs.GetName()) 21 | assert.Equal(t, 10, cs.Len()) 22 | x0, y0 := cs.GetValues(0) 23 | assert.InDelta(t, 1.0, x0, 0) 24 | assert.InDelta(t, 1.0, y0, 0) 25 | 26 | xn, yn := cs.GetValues(9) 27 | assert.InDelta(t, 10.0, xn, 0) 28 | assert.InDelta(t, 10.0, yn, 0) 29 | 30 | xn, yn = cs.GetLastValues() 31 | assert.InDelta(t, 10.0, xn, 0) 32 | assert.InDelta(t, 10.0, yn, 0) 33 | } 34 | 35 | func TestContinuousSeriesValueFormatter(t *testing.T) { 36 | t.Parallel() 37 | 38 | cs := ContinuousSeries{ 39 | XValueFormatter: func(v interface{}) string { 40 | return fmt.Sprintf("%f foo", v) 41 | }, 42 | YValueFormatter: func(v interface{}) string { 43 | return fmt.Sprintf("%f bar", v) 44 | }, 45 | } 46 | 47 | xf, yf := cs.GetValueFormatters() 48 | assert.Equal(t, "0.100000 foo", xf(0.1)) 49 | assert.Equal(t, "0.100000 bar", yf(0.1)) 50 | } 51 | 52 | func TestContinuousSeriesValidate(t *testing.T) { 53 | t.Parallel() 54 | 55 | cs := ContinuousSeries{ 56 | Name: "Test Series", 57 | XValues: LinearRange(1.0, 10.0), 58 | YValues: LinearRange(1.0, 10.0), 59 | } 60 | require.NoError(t, cs.Validate()) 61 | 62 | cs = ContinuousSeries{ 63 | Name: "Test Series", 64 | XValues: LinearRange(1.0, 10.0), 65 | } 66 | require.Error(t, cs.Validate()) 67 | 68 | cs = ContinuousSeries{ 69 | Name: "Test Series", 70 | YValues: LinearRange(1.0, 10.0), 71 | } 72 | require.Error(t, cs.Validate()) 73 | } 74 | -------------------------------------------------------------------------------- /chartdraw/donut_chart_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDonutChart(t *testing.T) { 12 | t.Parallel() 13 | 14 | pie := DonutChart{ 15 | Canvas: Style{ 16 | FillColor: ColorLightGray, 17 | }, 18 | Values: []Value{ 19 | {Value: 10, Label: "Blue"}, 20 | {Value: 9, Label: "Green"}, 21 | {Value: 8, Label: "Gray"}, 22 | {Value: 7, Label: "Orange"}, 23 | {Value: 6, Label: "HEANG"}, 24 | {Value: 5, Label: "??"}, 25 | {Value: 2, Label: "!!"}, 26 | }, 27 | } 28 | 29 | b := bytes.NewBuffer([]byte{}) 30 | require.NoError(t, pie.Render(PNG, b)) 31 | assert.NotZero(t, b.Len()) 32 | } 33 | 34 | func TestDonutChartDropsZeroValues(t *testing.T) { 35 | t.Parallel() 36 | 37 | pie := DonutChart{ 38 | Canvas: Style{ 39 | FillColor: ColorLightGray, 40 | }, 41 | Values: []Value{ 42 | {Value: 5, Label: "Blue"}, 43 | {Value: 5, Label: "Green"}, 44 | {Value: 0, Label: "Gray"}, 45 | }, 46 | } 47 | 48 | b := bytes.NewBuffer([]byte{}) 49 | require.NoError(t, pie.Render(PNG, b)) 50 | } 51 | 52 | func TestDonutChartAllZeroValues(t *testing.T) { 53 | t.Parallel() 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 | require.Error(t, pie.Render(PNG, b)) 68 | } 69 | -------------------------------------------------------------------------------- /chartdraw/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). -------------------------------------------------------------------------------- /chartdraw/drawing/curve_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type point struct { 11 | X, Y float64 12 | } 13 | 14 | type mockLine struct { 15 | inner []point 16 | } 17 | 18 | func (ml *mockLine) LineTo(x, y float64) { 19 | ml.inner = append(ml.inner, point{x, y}) 20 | } 21 | 22 | func (ml *mockLine) Len() int { 23 | return len(ml.inner) 24 | } 25 | 26 | func TestTraceQuad(t *testing.T) { 27 | t.Parallel() 28 | 29 | // Quad 30 | // x1, y1, cpx1, cpy2, x2, y2 float64 31 | // do the 9->12 circle segment 32 | quad := []float64{10, 20, 20, 20, 20, 10} 33 | liner := &mockLine{} 34 | TraceQuad(liner, quad, 0.5) 35 | assert.NotZero(t, liner.Len()) 36 | } 37 | 38 | func TestSubdivideCubic(t *testing.T) { 39 | t.Parallel() 40 | 41 | cubic := []float64{0, 0, 0, 1, 1, 1, 1, 0} 42 | c1 := make([]float64, 8) 43 | c2 := make([]float64, 8) 44 | SubdivideCubic(cubic, c1, c2) 45 | 46 | expectC1 := []float64{0, 0, 0, 0.5, 0.25, 0.75, 0.5, 0.75} 47 | expectC2 := []float64{0.5, 0.75, 0.75, 0.75, 1, 0.5, 1, 0} 48 | assert.InDeltaSlice(t, expectC1, c1, 0.0001) 49 | assert.InDeltaSlice(t, expectC2, c2, 0.0001) 50 | } 51 | 52 | func TestTraceCubicAndArc(t *testing.T) { 53 | t.Parallel() 54 | 55 | cubic := []float64{0, 0, 0, 1, 1, 1, 1, 0} 56 | liner := &mockLine{} 57 | TraceCubic(liner, cubic, 0.1) 58 | last := liner.inner[len(liner.inner)-1] 59 | assert.InDelta(t, 1.0, last.X, 0.0001) 60 | assert.InDelta(t, 0.0, last.Y, 0.0001) 61 | 62 | liner = &mockLine{} 63 | lx, ly := TraceArc(liner, 0, 0, 1, 1, 0, math.Pi/2, 1) 64 | assert.InDelta(t, 0.0, lx, 0.0001) 65 | assert.InDelta(t, 1.0, ly, 0.0001) 66 | assert.NotZero(t, liner.Len()) 67 | } 68 | -------------------------------------------------------------------------------- /chartdraw/drawing/dasher_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type recordFlattenerEnd struct { 11 | moves []string 12 | } 13 | 14 | func (r *recordFlattenerEnd) MoveTo(x, y float64) { 15 | r.moves = append(r.moves, fmt.Sprintf("M%.1f,%.1f", x, y)) 16 | } 17 | 18 | func (r *recordFlattenerEnd) LineTo(x, y float64) { 19 | r.moves = append(r.moves, fmt.Sprintf("L%.1f,%.1f", x, y)) 20 | } 21 | 22 | func (r *recordFlattenerEnd) End() { 23 | r.moves = append(r.moves, "E") 24 | } 25 | 26 | func TestDashVertexConverterLineTo(t *testing.T) { 27 | t.Parallel() 28 | 29 | rec := &recordFlattenerEnd{} 30 | d := NewDashVertexConverter([]float64{2, 2}, 0, rec) 31 | d.MoveTo(0, 0) 32 | d.LineTo(5, 0) 33 | d.End() 34 | 35 | expect := []string{"M0.0,0.0", "L2.0,0.0", "E", "M4.0,0.0", "L5.0,0.0", "E"} 36 | assert.Equal(t, expect, rec.moves) 37 | } 38 | -------------------------------------------------------------------------------- /chartdraw/drawing/demux_flattener.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // DemuxFlattener is a slice of 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 | // End implements the path builder interface. 23 | func (dc DemuxFlattener) End() { 24 | for _, flattener := range dc.Flatteners { 25 | flattener.End() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chartdraw/drawing/demux_flattener_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "golang.org/x/image/math/fixed" 8 | ) 9 | 10 | type recordAdder struct{ starts, adds []fixed.Point26_6 } 11 | 12 | func (r *recordAdder) Start(p fixed.Point26_6) { r.starts = append(r.starts, p) } 13 | func (r *recordAdder) Add1(p fixed.Point26_6) { r.adds = append(r.adds, p) } 14 | func (r *recordAdder) Add2(b, c fixed.Point26_6) {} 15 | func (r *recordAdder) Add3(b, c, d fixed.Point26_6) {} 16 | 17 | func TestDemuxFlattener(t *testing.T) { 18 | t.Parallel() 19 | 20 | r1 := &recordFlattener{} 21 | r2 := &recordFlattener{} 22 | d := DemuxFlattener{Flatteners: []Flattener{r1, r2}} 23 | d.MoveTo(1, 2) 24 | d.LineTo(3, 4) 25 | d.End() 26 | assert.Equal(t, r1.moves, r2.moves) 27 | assert.Equal(t, []string{"M1.0,2.0", "L3.0,4.0"}, r1.moves) 28 | } 29 | 30 | func TestFtLineBuilder(t *testing.T) { 31 | t.Parallel() 32 | 33 | ad := &recordAdder{} 34 | ft := FtLineBuilder{Adder: ad} 35 | ft.MoveTo(1, 1) 36 | ft.LineTo(2, 3) 37 | ft.End() 38 | if assert.Len(t, ad.starts, 1) { 39 | assert.Equal(t, fixed.Int26_6(64), ad.starts[0].X) 40 | assert.Equal(t, fixed.Int26_6(64), ad.starts[0].Y) 41 | } 42 | if assert.Len(t, ad.adds, 1) { 43 | assert.Equal(t, fixed.Int26_6(128), ad.adds[0].X) 44 | assert.Equal(t, fixed.Int26_6(192), ad.adds[0].Y) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chartdraw/drawing/drawing.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | // FillRule defines the type for fill rules 4 | type FillRule int 5 | 6 | const ( 7 | // FillRuleEvenOdd determines the "insideness" of a point in the shape 8 | // by drawing a ray from that point to infinity in any direction 9 | // and counting the number of path segments from the given shape that the ray crosses. 10 | // If this number is odd, the point is inside; if even, the point is outside. 11 | FillRuleEvenOdd FillRule = iota 12 | // FillRuleWinding determines the "insideness" of a point in the shape 13 | // by drawing a ray from that point to infinity in any direction 14 | // and then examining the places where a segment of the shape crosses the ray. 15 | // Starting with a count of zero, add one each time a path segment crosses 16 | // the ray from left to right and subtract one each time 17 | // a path segment crosses the ray from right to left. After counting the crossings, 18 | // if the result is zero then the point is outside the path. Otherwise, it is inside. 19 | FillRuleWinding 20 | ) 21 | -------------------------------------------------------------------------------- /chartdraw/drawing/flattener_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type recordFloat struct { 11 | ops []string 12 | xs []float64 13 | ys []float64 14 | ends int 15 | } 16 | 17 | func (r *recordFloat) MoveTo(x, y float64) { 18 | r.ops = append(r.ops, "M") 19 | r.xs = append(r.xs, x) 20 | r.ys = append(r.ys, y) 21 | } 22 | 23 | func (r *recordFloat) LineTo(x, y float64) { 24 | r.ops = append(r.ops, "L") 25 | r.xs = append(r.xs, x) 26 | r.ys = append(r.ys, y) 27 | } 28 | 29 | func (r *recordFloat) End() { 30 | r.ends++ 31 | } 32 | 33 | func TestFlattenMixed(t *testing.T) { 34 | t.Parallel() 35 | 36 | p := &Path{} 37 | p.MoveTo(0, 0) 38 | p.LineTo(1, 0) 39 | p.QuadCurveTo(1.5, 0, 2, 0) 40 | p.CubicCurveTo(2.5, 0, 2.75, 0, 3, 0) 41 | p.ArcTo(4, 0, 1, 1, 0, math.Pi/2) 42 | p.Close() 43 | 44 | rec := &recordFloat{} 45 | Flatten(p, rec, 1.0) 46 | 47 | expectOps := []string{"M", "L", "L", "L", "L", "L", "L", "L", "L"} 48 | expectX := []float64{0, 1, 2, 3, 3, 5, 4.580247, 4, 0} 49 | expectY := []float64{0, 0, 0, 0, 0, 0, 0.814441, 1, 0} 50 | 51 | assert.Equal(t, 1, rec.ends) 52 | assert.Equal(t, expectOps, rec.ops) 53 | assert.InDeltaSlice(t, expectX, rec.xs, 0.0001) 54 | assert.InDeltaSlice(t, expectY, rec.ys, 0.0001) 55 | } 56 | 57 | func TestFlattenMultiMove(t *testing.T) { 58 | t.Parallel() 59 | 60 | p := &Path{} 61 | p.MoveTo(0, 0) 62 | p.LineTo(1, 0) 63 | p.MoveTo(2, 0) 64 | p.LineTo(3, 0) 65 | 66 | rec := &recordFloat{} 67 | Flatten(p, rec, 1.0) 68 | 69 | expectOps := []string{"M", "L", "M", "L"} 70 | expectX := []float64{0, 1, 2, 3} 71 | expectY := []float64{0, 0, 0, 0} 72 | 73 | assert.Equal(t, 2, rec.ends) 74 | assert.Equal(t, expectOps, rec.ops) 75 | assert.InDeltaSlice(t, expectX, rec.xs, 0.0001) 76 | assert.InDeltaSlice(t, expectY, rec.ys, 0.0001) 77 | } 78 | -------------------------------------------------------------------------------- /chartdraw/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 | // End implements the path builder interface. 24 | func (liner FtLineBuilder) End() {} 25 | -------------------------------------------------------------------------------- /chartdraw/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 | -------------------------------------------------------------------------------- /chartdraw/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 := absInt(x1 - x0) 18 | dy := absInt(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 -= dy 41 | x0 += sx 42 | } 43 | if e2 < dx { 44 | err += dx 45 | y0 += sy 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chartdraw/drawing/line_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBresenhamDiagonal(t *testing.T) { 12 | t.Parallel() 13 | 14 | img := image.NewRGBA(image.Rect(0, 0, 5, 5)) 15 | Bresenham(img, color.White, 0, 0, 4, 4) 16 | 17 | for i := 0; i <= 4; i++ { 18 | r, g, b, a := img.At(i, i).RGBA() 19 | assert.Equal(t, uint32(0xffff), r) 20 | assert.Equal(t, uint32(0xffff), g) 21 | assert.Equal(t, uint32(0xffff), b) 22 | assert.Equal(t, uint32(0xffff), a) 23 | } 24 | 25 | _, _, _, a := img.At(0, 1).RGBA() 26 | assert.Equal(t, uint32(0), a) 27 | } 28 | 29 | func TestPolylineBresenham(t *testing.T) { 30 | t.Parallel() 31 | 32 | img := image.NewRGBA(image.Rect(0, 0, 5, 5)) 33 | PolylineBresenham(img, color.White, 0, 0, 2, 0, 2, 2) 34 | 35 | expected := [][2]int{{0, 0}, {1, 0}, {2, 0}, {2, 1}, {2, 2}} 36 | for _, p := range expected { 37 | _, _, _, a := img.At(p[0], p[1]).RGBA() 38 | assert.Equal(t, uint32(0xffff), a) 39 | } 40 | 41 | _, _, _, a := img.At(1, 1).RGBA() 42 | assert.Equal(t, uint32(0), a) 43 | } 44 | -------------------------------------------------------------------------------- /chartdraw/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 | -------------------------------------------------------------------------------- /chartdraw/drawing/painter_draw_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/image/draw" 10 | ) 11 | 12 | func TestDrawImageTransform(t *testing.T) { 13 | t.Parallel() 14 | 15 | src := image.NewRGBA(image.Rect(0, 0, 1, 1)) 16 | src.Set(0, 0, color.White) 17 | dst := image.NewRGBA(image.Rect(0, 0, 3, 3)) 18 | DrawImage(src, dst, NewTranslationMatrix(1, 1), draw.Over, LinearFilter) 19 | _, _, _, a := dst.At(1, 1).RGBA() 20 | assert.Equal(t, uint32(0xffff), a) 21 | } 22 | 23 | func TestDrawImageScale(t *testing.T) { 24 | t.Parallel() 25 | 26 | src := image.NewRGBA(image.Rect(0, 0, 1, 1)) 27 | src.Set(0, 0, color.White) 28 | dst := image.NewRGBA(image.Rect(0, 0, 2, 2)) 29 | DrawImage(src, dst, NewScaleMatrix(2, 2), draw.Over, LinearFilter) 30 | _, _, _, a := dst.At(1, 1).RGBA() 31 | assert.Equal(t, uint32(0xffff), a) 32 | } 33 | -------------------------------------------------------------------------------- /chartdraw/drawing/stack_graphic_context_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStackGraphicContextSaveRestore(t *testing.T) { 11 | t.Parallel() 12 | 13 | gc := NewStackGraphicContext() 14 | gc.SetLineWidth(2) 15 | gc.MoveTo(1, 1) 16 | gc.Save() 17 | gc.SetLineWidth(4) 18 | gc.LineTo(2, 2) 19 | gc.Restore() 20 | assert.InDelta(t, 2.0, gc.current.LineWidth, 0.0001) 21 | x, y := gc.LastPoint() 22 | assert.InDelta(t, 1.0, x, 0.0001) 23 | assert.InDelta(t, 1.0, y, 0.0001) 24 | } 25 | 26 | func TestStackGraphicContextTransforms(t *testing.T) { 27 | t.Parallel() 28 | 29 | gc := NewStackGraphicContext() 30 | gc.Translate(2, 3) 31 | tr := gc.GetMatrixTransform() 32 | x, y := tr.TransformPoint(0, 0) 33 | assert.InDelta(t, 2.0, x, 0.0001) 34 | assert.InDelta(t, 3.0, y, 0.0001) 35 | gc.Rotate(math.Pi / 2) 36 | tr = gc.GetMatrixTransform() 37 | x, y = tr.TransformPoint(1, 0) 38 | assert.InDelta(t, 2.0, x, 0.0001) 39 | assert.InDelta(t, 4.0, y, 0.0001) 40 | } 41 | 42 | func TestStackGraphicContextColors(t *testing.T) { 43 | t.Parallel() 44 | 45 | gc := NewStackGraphicContext() 46 | gc.SetStrokeColor(ColorRed) 47 | gc.SetFillColor(ColorBlue) 48 | assert.Equal(t, ColorRed, gc.current.StrokeColor) 49 | assert.Equal(t, ColorBlue, gc.current.FillColor) 50 | } 51 | 52 | func TestStackMatrixTransform(t *testing.T) { 53 | t.Parallel() 54 | 55 | gc := NewStackGraphicContext() 56 | tr := NewTranslationMatrix(5, 7) 57 | gc.SetMatrixTransform(tr) 58 | got := gc.GetMatrixTransform() 59 | x, y := got.TransformPoint(0, 0) 60 | assert.InDelta(t, 5.0, x, 0.0001) 61 | assert.InDelta(t, 7.0, y, 0.0001) 62 | } 63 | -------------------------------------------------------------------------------- /chartdraw/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(flattener Flattener) *LineStroker { 8 | l := new(LineStroker) 9 | l.Flattener = flattener 10 | l.HalfLineWidth = 0.5 11 | return l 12 | } 13 | 14 | // LineStroker draws the stroke portion of a line. 15 | type LineStroker struct { 16 | Flattener Flattener 17 | HalfLineWidth float64 18 | vertices []float64 19 | rewind []float64 20 | x, y, nx, ny float64 21 | } 22 | 23 | // MoveTo implements the path builder interface. 24 | func (l *LineStroker) MoveTo(x, y float64) { 25 | l.x, l.y = x, y 26 | } 27 | 28 | // LineTo implements the path builder interface. 29 | func (l *LineStroker) LineTo(x, y float64) { 30 | l.line(l.x, l.y, x, y) 31 | } 32 | 33 | func (l *LineStroker) line(x1, y1, x2, y2 float64) { 34 | dx := x2 - x1 35 | dy := y2 - y1 36 | d := vectorDistance(dx, dy) 37 | if d != 0 { 38 | nx := dy * l.HalfLineWidth / d 39 | ny := -(dx * l.HalfLineWidth / d) 40 | l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny) 41 | l.x, l.y, l.nx, l.ny = x2, y2, nx, ny 42 | } 43 | } 44 | 45 | // End implements the path builder interface. 46 | func (l *LineStroker) End() { 47 | if len(l.vertices) > 1 { 48 | l.Flattener.MoveTo(l.vertices[0], l.vertices[1]) 49 | for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 { 50 | l.Flattener.LineTo(l.vertices[i], l.vertices[j]) 51 | } 52 | } 53 | for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 { 54 | l.Flattener.LineTo(l.rewind[i], l.rewind[j]) 55 | } 56 | if len(l.vertices) > 1 { 57 | l.Flattener.LineTo(l.vertices[0], l.vertices[1]) 58 | } 59 | l.Flattener.End() 60 | // reinit vertices 61 | l.vertices = l.vertices[0:0] 62 | l.rewind = l.rewind[0:0] 63 | l.x, l.y, l.nx, l.ny = 0, 0, 0, 0 64 | } 65 | 66 | func (l *LineStroker) appendVertex(vertices ...float64) { 67 | s := len(vertices) / 2 68 | l.vertices = append(l.vertices, vertices[:s]...) 69 | l.rewind = append(l.rewind, vertices[s:]...) 70 | } 71 | -------------------------------------------------------------------------------- /chartdraw/drawing/stroker_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLineStrokerLine(t *testing.T) { 10 | t.Parallel() 11 | 12 | rec := &recordFlattenerEnd{} 13 | ls := NewLineStroker(rec) 14 | ls.MoveTo(0, 0) 15 | ls.LineTo(2, 0) 16 | ls.End() 17 | 18 | expect := []string{"M0.0,-0.5", "L2.0,-0.5", "L2.0,0.5", "L0.0,0.5", "L0.0,-0.5", "E"} 19 | assert.Equal(t, expect, rec.moves) 20 | } 21 | -------------------------------------------------------------------------------- /chartdraw/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 | -------------------------------------------------------------------------------- /chartdraw/drawing/text_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/go-analyze/charts/chartdraw/roboto" 8 | "github.com/golang/freetype/truetype" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "golang.org/x/image/math/fixed" 12 | ) 13 | 14 | type recordBuilder struct{ ops []string } 15 | 16 | func (r *recordBuilder) LastPoint() (float64, float64) { return 0, 0 } 17 | func (r *recordBuilder) MoveTo(x, y float64) { r.ops = append(r.ops, fmt.Sprintf("M%.1f,%.1f", x, y)) } 18 | func (r *recordBuilder) LineTo(x, y float64) { r.ops = append(r.ops, fmt.Sprintf("L%.1f,%.1f", x, y)) } 19 | func (r *recordBuilder) QuadCurveTo(cx, cy, x, y float64) { 20 | r.ops = append(r.ops, fmt.Sprintf("Q%.1f,%.1f,%.1f,%.1f", cx, cy, x, y)) 21 | } 22 | func (r *recordBuilder) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {} 23 | func (r *recordBuilder) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {} 24 | func (r *recordBuilder) Close() {} 25 | 26 | func TestDrawContour(t *testing.T) { 27 | t.Parallel() 28 | 29 | contour := []truetype.Point{ 30 | {X: 0, Y: 0, Flags: 0x01}, 31 | {X: 64, Y: 0, Flags: 0x01}, 32 | {X: 64, Y: 64, Flags: 0x00}, 33 | {X: 0, Y: 64, Flags: 0x01}, 34 | } 35 | 36 | rec := &recordBuilder{} 37 | DrawContour(rec, contour, 0, 0) 38 | 39 | expect := []string{ 40 | "M0.0,0.0", 41 | "L1.0,0.0", 42 | "Q1.0,-1.0,0.0,-1.0", 43 | "L0.0,0.0", 44 | } 45 | 46 | assert.Equal(t, expect, rec.ops) 47 | } 48 | 49 | func TestFontExtents(t *testing.T) { 50 | t.Parallel() 51 | 52 | f, err := truetype.Parse(roboto.Roboto) 53 | require.NoError(t, err) 54 | ext := Extents(f, 10) 55 | bounds := f.Bounds(fixed.Int26_6(f.FUnitsPerEm())) 56 | scale := 10 / float64(f.FUnitsPerEm()) 57 | assert.InDelta(t, float64(bounds.Max.Y)*scale, ext.Ascent, 0.0001) 58 | assert.InDelta(t, float64(bounds.Min.Y)*scale, ext.Descent, 0.0001) 59 | assert.InDelta(t, float64(bounds.Max.Y-bounds.Min.Y)*scale, ext.Height, 0.0001) 60 | } 61 | -------------------------------------------------------------------------------- /chartdraw/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 | // End implements the path builder interface. 27 | func (t Transformer) End() { 28 | t.Flattener.End() 29 | } 30 | -------------------------------------------------------------------------------- /chartdraw/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/truetype" 9 | ) 10 | 11 | // PointsToPixels returns the pixels for a given number of points at a DPI. 12 | func PointsToPixels(dpi, points float64) (pixels float64) { 13 | return (points * dpi) / 72.0 14 | } 15 | 16 | func absInt(i int) int { 17 | if i < 0 { 18 | return -i 19 | } 20 | return i 21 | } 22 | 23 | func distance(x1, y1, x2, y2 float64) float64 { 24 | return vectorDistance(x2-x1, y2-y1) 25 | } 26 | 27 | func vectorDistance(dx, dy float64) float64 { 28 | return math.Sqrt(dx*dx + dy*dy) 29 | } 30 | 31 | func pointToF64Point(p truetype.Point) (x, y float64) { 32 | return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y) 33 | } 34 | 35 | func fUnitsToFloat64(x fixed.Int26_6) float64 { 36 | scaled := x << 2 37 | return float64(scaled/256) + float64(scaled%256)/256.0 38 | } 39 | -------------------------------------------------------------------------------- /chartdraw/drawing/util_test.go: -------------------------------------------------------------------------------- 1 | package drawing 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/golang/freetype/truetype" 8 | "github.com/stretchr/testify/assert" 9 | "golang.org/x/image/math/fixed" 10 | ) 11 | 12 | func TestPointsToPixels(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | dpi, points float64 17 | want float64 18 | }{ 19 | {72, 72, 72}, 20 | {96, 72, 96}, 21 | {96, 36, 48}, 22 | } 23 | for i, tt := range tests { 24 | t.Run(strconv.Itoa(i), func(t *testing.T) { 25 | got := PointsToPixels(tt.dpi, tt.points) 26 | assert.InDelta(t, tt.want, got, 0.0001) 27 | }) 28 | } 29 | } 30 | 31 | func TestDistanceFuncs(t *testing.T) { 32 | t.Parallel() 33 | 34 | tests := []struct { 35 | x1, y1, x2, y2 float64 36 | want float64 37 | }{ 38 | {0, 0, 3, 4, 5}, 39 | {1, 2, 1, 2, 0}, 40 | {-1, -1, -4, -5, 5}, 41 | } 42 | for i, tt := range tests { 43 | t.Run(strconv.Itoa(i), func(t *testing.T) { 44 | assert.InDelta(t, tt.want, distance(tt.x1, tt.y1, tt.x2, tt.y2), 0.0001) 45 | }) 46 | } 47 | } 48 | 49 | func TestVectorDistance(t *testing.T) { 50 | t.Parallel() 51 | tests := []struct{ dx, dy, want float64 }{ 52 | {3, 4, 5}, 53 | {0, 0, 0}, 54 | } 55 | for i, tt := range tests { 56 | t.Run(strconv.Itoa(i), func(t *testing.T) { 57 | assert.InDelta(t, tt.want, vectorDistance(tt.dx, tt.dy), 0.0001) 58 | }) 59 | } 60 | } 61 | 62 | func TestFUnitsConversion(t *testing.T) { 63 | t.Parallel() 64 | 65 | tests := []struct { 66 | in fixed.Int26_6 67 | want float64 68 | }{ 69 | {64, 1}, 70 | {96, 1.5}, 71 | {-64, -1}, 72 | } 73 | for i, tt := range tests { 74 | t.Run(strconv.Itoa(i), func(t *testing.T) { 75 | assert.InDelta(t, tt.want, fUnitsToFloat64(tt.in), 0.0001) 76 | }) 77 | } 78 | } 79 | 80 | func TestPointToF64Point(t *testing.T) { 81 | t.Parallel() 82 | 83 | p := truetype.Point{X: 128, Y: -64} 84 | x, y := pointToF64Point(p) 85 | assert.InDelta(t, 2.0, x, 0.0001) 86 | // Y is negated inside function 87 | assert.InDelta(t, 1.0, y, 0.0001) 88 | } 89 | -------------------------------------------------------------------------------- /chartdraw/examples/annotations/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 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 := chartdraw.Chart{ 24 | Series: []chartdraw.Series{ 25 | chartdraw.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 | chartdraw.AnnotationSeries{ 30 | Annotations: []chartdraw.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(chartdraw.PNG, f) 44 | } 45 | -------------------------------------------------------------------------------- /chartdraw/examples/annotations/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/annotations/output.png -------------------------------------------------------------------------------- /chartdraw/examples/axes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 14 | 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. 15 | */ 16 | 17 | graph := chartdraw.Chart{ 18 | Series: []chartdraw.Series{ 19 | chartdraw.ContinuousSeries{ 20 | Style: chartdraw.Style{ 21 | StrokeColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 22 | FillColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 23 | }, 24 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 25 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 26 | }, 27 | }, 28 | } 29 | 30 | f, _ := os.Create("output.png") 31 | defer f.Close() 32 | graph.Render(chartdraw.PNG, f) 33 | } 34 | -------------------------------------------------------------------------------- /chartdraw/examples/axes/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/axes/output.png -------------------------------------------------------------------------------- /chartdraw/examples/axes_labels/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 14 | 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. 15 | */ 16 | 17 | graph := chartdraw.Chart{ 18 | XAxis: chartdraw.XAxis{ 19 | Name: "The XAxis", 20 | }, 21 | YAxis: chartdraw.YAxis{ 22 | Name: "The YAxis", 23 | }, 24 | Series: []chartdraw.Series{ 25 | chartdraw.ContinuousSeries{ 26 | Style: chartdraw.Style{ 27 | StrokeColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 28 | FillColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 29 | }, 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 | 36 | f, _ := os.Create("output.png") 37 | defer f.Close() 38 | graph.Render(chartdraw.PNG, f) 39 | } 40 | -------------------------------------------------------------------------------- /chartdraw/examples/axes_labels/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/axes_labels/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | graph := chartdraw.BarChart{ 13 | Title: "Test Bar Chart", 14 | Background: chartdraw.Style{ 15 | Padding: chartdraw.Box{ 16 | Top: 40, 17 | }, 18 | }, 19 | Height: 512, 20 | BarWidth: 60, 21 | Bars: []chartdraw.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(chartdraw.PNG, f) 35 | } 36 | -------------------------------------------------------------------------------- /chartdraw/examples/bar_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/bar_chart/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | "github.com/go-analyze/charts/chartdraw/drawing" 10 | ) 11 | 12 | func main() { 13 | profitStyle := chartdraw.Style{ 14 | FillColor: drawing.ColorFromHex("13c158"), 15 | StrokeColor: drawing.ColorFromHex("13c158"), 16 | StrokeWidth: 0, 17 | } 18 | 19 | lossStyle := chartdraw.Style{ 20 | FillColor: drawing.ColorFromHex("c11313"), 21 | StrokeColor: drawing.ColorFromHex("c11313"), 22 | StrokeWidth: 0, 23 | } 24 | 25 | sbc := chartdraw.BarChart{ 26 | Title: "Bar Chart Using BaseValue", 27 | Background: chartdraw.Style{ 28 | Padding: chartdraw.Box{ 29 | Top: 40, 30 | }, 31 | }, 32 | Height: 512, 33 | BarWidth: 60, 34 | YAxis: chartdraw.YAxis{ 35 | Ticks: []chartdraw.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: []chartdraw.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(chartdraw.PNG, f) 62 | } 63 | -------------------------------------------------------------------------------- /chartdraw/examples/bar_chart_base_value/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/bar_chart_base_value/output.png -------------------------------------------------------------------------------- /chartdraw/examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | graph := chartdraw.Chart{ 13 | Series: []chartdraw.Series{ 14 | chartdraw.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(chartdraw.PNG, f) 23 | } 24 | -------------------------------------------------------------------------------- /chartdraw/examples/basic/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/basic/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 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([]chartdraw.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] = chartdraw.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 := chartdraw.Chart{ 40 | XAxis: chartdraw.XAxis{ 41 | Name: "Time", 42 | }, 43 | YAxis: chartdraw.YAxis{ 44 | Name: "Value", 45 | }, 46 | Series: series, 47 | } 48 | 49 | f, _ := os.Create("output.png") 50 | defer f.Close() 51 | graph.Render(chartdraw.PNG, f) 52 | } 53 | -------------------------------------------------------------------------------- /chartdraw/examples/benchmark_line_charts/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/benchmark_line_charts/output.png -------------------------------------------------------------------------------- /chartdraw/examples/css_classes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 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 := chartdraw.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: chartdraw.Style{ClassName: "background"}, 24 | Canvas: chartdraw.Style{ 25 | ClassName: "canvas", 26 | }, 27 | Width: 512, 28 | Height: 512, 29 | Values: []chartdraw.Value{ 30 | {Value: 5, Label: "Blue", Style: chartdraw.Style{ClassName: "blue"}}, 31 | {Value: 5, Label: "Green", Style: chartdraw.Style{ClassName: "green"}}, 32 | {Value: 4, Label: "Gray", Style: chartdraw.Style{ClassName: "gray"}}, 33 | }, 34 | } 35 | 36 | if err := pie.Render(chartdraw.SVG, res); err != nil { 37 | fmt.Printf("Error rendering pie chart: %v\n", err) 38 | } 39 | res.Write([]byte("")) 40 | } 41 | 42 | func css(res http.ResponseWriter, req *http.Request) { 43 | res.Header().Set("Content-Type", "text/css") 44 | res.Write([]byte("svg .background { fill: white; }" + 45 | "svg .canvas { fill: white; }" + 46 | "svg .blue.fill.stroke { fill: blue; stroke: lightblue; }" + 47 | "svg .green.fill.stroke { fill: green; stroke: lightgreen; }" + 48 | "svg .gray.fill.stroke { fill: gray; stroke: lightgray; }" + 49 | "svg .blue.text { fill: white; }" + 50 | "svg .green.text { fill: white; }" + 51 | "svg .gray.text { fill: white; }")) 52 | } 53 | 54 | func main() { 55 | http.HandleFunc("/", inlineSVGWithClasses) 56 | http.HandleFunc("/main.css", css) 57 | log.Fatal(http.ListenAndServe(":8080", nil)) 58 | } 59 | -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 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 := chartdraw.Chart{ 20 | YAxis: chartdraw.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: []chartdraw.Series{ 29 | chartdraw.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(chartdraw.PNG, f) 38 | } 39 | -------------------------------------------------------------------------------- /chartdraw/examples/custom_formatters/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/custom_formatters/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | "github.com/go-analyze/charts/chartdraw/drawing" 10 | ) 11 | 12 | func main() { 13 | graph := chartdraw.Chart{ 14 | Background: chartdraw.Style{ 15 | Padding: chartdraw.Box{ 16 | Top: 50, 17 | Left: 25, 18 | Right: 25, 19 | Bottom: 10, 20 | }, 21 | FillColor: drawing.ColorFromHex("efefef"), 22 | }, 23 | Series: []chartdraw.Series{ 24 | chartdraw.ContinuousSeries{ 25 | XValues: chartdraw.Seq{Sequence: chartdraw.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), 26 | YValues: chartdraw.Seq{Sequence: chartdraw.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(chartdraw.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /chartdraw/examples/custom_padding/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/custom_padding/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 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 := chartdraw.Chart{ 18 | YAxis: chartdraw.YAxis{ 19 | Range: &chartdraw.ContinuousRange{ 20 | Min: 0.0, 21 | Max: 10.0, 22 | }, 23 | }, 24 | Series: []chartdraw.Series{ 25 | chartdraw.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(chartdraw.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /chartdraw/examples/custom_ranges/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/custom_ranges/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | "github.com/go-analyze/charts/chartdraw/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 := chartdraw.Chart{ 17 | Background: chartdraw.Style{ 18 | FillColor: drawing.ColorBlue, 19 | }, 20 | Canvas: chartdraw.Style{ 21 | FillColor: drawing.ColorFromHex("efefef"), 22 | }, 23 | Series: []chartdraw.Series{ 24 | chartdraw.ContinuousSeries{ 25 | Style: chartdraw.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(chartdraw.PNG, f) 38 | } 39 | -------------------------------------------------------------------------------- /chartdraw/examples/custom_styles/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/custom_styles/output.png -------------------------------------------------------------------------------- /chartdraw/examples/custom_stylesheets/inlineOutput.svg: -------------------------------------------------------------------------------- 1 | \nBlueGreenGray -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 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 := chartdraw.Chart{ 18 | YAxis: chartdraw.YAxis{ 19 | Range: &chartdraw.ContinuousRange{ 20 | Min: 0.0, 21 | Max: 4.0, 22 | }, 23 | Ticks: []chartdraw.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: []chartdraw.Series{ 33 | chartdraw.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(chartdraw.PNG, f) 42 | } 43 | -------------------------------------------------------------------------------- /chartdraw/examples/custom_ticks/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/custom_ticks/output.png -------------------------------------------------------------------------------- /chartdraw/examples/descending/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. 14 | In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, 15 | the canvas "box" is adjusted to fit the space the axes occupy so as not to clip. 16 | Additionally, it shows how you can use the "Descending" property of continuous ranges to change the ordering of 17 | how values (including ticks) are drawn. 18 | */ 19 | 20 | graph := chartdraw.Chart{ 21 | Height: 500, 22 | Width: 500, 23 | XAxis: chartdraw.XAxis{ 24 | /*Range: &chartdraw.ContinuousRange{ 25 | Descending: true, 26 | },*/ 27 | }, 28 | YAxis: chartdraw.YAxis{ 29 | Range: &chartdraw.ContinuousRange{ 30 | Descending: true, 31 | }, 32 | }, 33 | Series: []chartdraw.Series{ 34 | chartdraw.ContinuousSeries{ 35 | Style: chartdraw.Style{ 36 | StrokeColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 37 | FillColor: chartdraw.GetDefaultColor(0).WithAlpha(64), 38 | }, 39 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 40 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 41 | }, 42 | }, 43 | } 44 | 45 | f, _ := os.Create("output.png") 46 | defer f.Close() 47 | graph.Render(chartdraw.PNG, f) 48 | } 49 | -------------------------------------------------------------------------------- /chartdraw/examples/descending/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/descending/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | pie := chartdraw.DonutChart{ 13 | Width: 512, 14 | Height: 512, 15 | Values: []chartdraw.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(chartdraw.PNG, f) 28 | } 29 | -------------------------------------------------------------------------------- /chartdraw/examples/donut_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/donut_chart/output.png -------------------------------------------------------------------------------- /chartdraw/examples/donut_chart/reg.svg: -------------------------------------------------------------------------------- 1 | \nBlueTwoOne -------------------------------------------------------------------------------- /chartdraw/examples/horizontal_stacked_bar/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/horizontal_stacked_bar/output.png -------------------------------------------------------------------------------- /chartdraw/examples/image_writer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/go-analyze/charts/chartdraw" 8 | ) 9 | 10 | func main() { 11 | graph := chartdraw.Chart{ 12 | Series: []chartdraw.Series{ 13 | chartdraw.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 := &chartdraw.ImageWriter{} 20 | graph.Render(chartdraw.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 | -------------------------------------------------------------------------------- /chartdraw/examples/legend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we add a `Renderable` or a custom component to the `Elements` array. 14 | In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series. 15 | If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend. 16 | */ 17 | 18 | graph := chartdraw.Chart{ 19 | Background: chartdraw.Style{ 20 | Padding: chartdraw.Box{ 21 | Top: 20, 22 | Left: 20, 23 | }, 24 | }, 25 | Series: []chartdraw.Series{ 26 | chartdraw.ContinuousSeries{ 27 | Name: "A test series", 28 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 29 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 30 | }, 31 | }, 32 | } 33 | 34 | //note we have to do this as a separate step because we need a reference to graph 35 | graph.Elements = []chartdraw.Renderable{ 36 | chartdraw.Legend(&graph), 37 | } 38 | 39 | f, _ := os.Create("output.png") 40 | defer f.Close() 41 | graph.Render(chartdraw.PNG, f) 42 | } 43 | -------------------------------------------------------------------------------- /chartdraw/examples/legend/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/legend/output.png -------------------------------------------------------------------------------- /chartdraw/examples/legend_left/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/legend_left/output.png -------------------------------------------------------------------------------- /chartdraw/examples/linear_regression/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument. 14 | InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. 15 | */ 16 | 17 | mainSeries := chartdraw.ContinuousSeries{ 18 | Name: "A test series", 19 | XValues: chartdraw.Seq{Sequence: chartdraw.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. 20 | YValues: chartdraw.Seq{Sequence: chartdraw.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements. 21 | } 22 | 23 | // note we create a LinearRegressionSeries series by assignin the inner series. 24 | // we need to use a reference because `.Render()` needs to modify state within the series. 25 | linRegSeries := &chartdraw.LinearRegressionSeries{ 26 | InnerSeries: mainSeries, 27 | } // we can optionally set the `WindowSize` property which alters how the moving average is calculated. 28 | 29 | graph := chartdraw.Chart{ 30 | Series: []chartdraw.Series{ 31 | mainSeries, 32 | linRegSeries, 33 | }, 34 | } 35 | 36 | f, _ := os.Create("output.png") 37 | defer f.Close() 38 | graph.Render(chartdraw.PNG, f) 39 | } 40 | -------------------------------------------------------------------------------- /chartdraw/examples/linear_regression/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/linear_regression/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | /* 13 | In this example we set the primary YAxis to have logarithmic range. 14 | */ 15 | 16 | graph := chartdraw.Chart{ 17 | Background: chartdraw.Style{ 18 | Padding: chartdraw.Box{ 19 | Top: 20, 20 | Left: 20, 21 | }, 22 | }, 23 | Series: []chartdraw.Series{ 24 | chartdraw.ContinuousSeries{ 25 | Name: "A test series", 26 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 27 | YValues: []float64{1, 10, 100, 1000, 10000}, 28 | }, 29 | }, 30 | YAxis: chartdraw.YAxis{ 31 | Style: chartdraw.Shown(), 32 | NameStyle: chartdraw.Shown(), 33 | Range: &chartdraw.LogarithmicRange{}, 34 | }, 35 | } 36 | 37 | f, _ := os.Create("output.png") 38 | defer f.Close() 39 | graph.Render(chartdraw.PNG, f) 40 | } 41 | -------------------------------------------------------------------------------- /chartdraw/examples/logarithmic_axes/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/logarithmic_axes/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | mainSeries := chartdraw.ContinuousSeries{ 13 | Name: "A test series", 14 | XValues: chartdraw.Seq{Sequence: chartdraw.NewLinearSequence().WithStart(1.0).WithEnd(100.0)}.Values(), 15 | YValues: chartdraw.Seq{Sequence: chartdraw.NewRandomSequence().WithLen(100).WithMin(50).WithMax(150)}.Values(), 16 | } 17 | 18 | minSeries := &chartdraw.MinSeries{ 19 | Style: chartdraw.Style{ 20 | StrokeColor: chartdraw.ColorAlternateGray, 21 | StrokeDashArray: []float64{5.0, 5.0}, 22 | }, 23 | InnerSeries: mainSeries, 24 | } 25 | 26 | maxSeries := &chartdraw.MaxSeries{ 27 | Style: chartdraw.Style{ 28 | StrokeColor: chartdraw.ColorAlternateGray, 29 | StrokeDashArray: []float64{5.0, 5.0}, 30 | }, 31 | InnerSeries: mainSeries, 32 | } 33 | 34 | graph := chartdraw.Chart{ 35 | Width: 1920, 36 | Height: 1080, 37 | YAxis: chartdraw.YAxis{ 38 | Name: "Random Values", 39 | Range: &chartdraw.ContinuousRange{ 40 | Min: 25, 41 | Max: 175, 42 | }, 43 | }, 44 | XAxis: chartdraw.XAxis{ 45 | Name: "Random Other Values", 46 | }, 47 | Series: []chartdraw.Series{ 48 | mainSeries, 49 | minSeries, 50 | maxSeries, 51 | chartdraw.LastValueAnnotationSeries(minSeries), 52 | chartdraw.LastValueAnnotationSeries(maxSeries), 53 | }, 54 | } 55 | 56 | graph.Elements = []chartdraw.Renderable{chartdraw.Legend(&graph)} 57 | 58 | f, _ := os.Create("output.png") 59 | defer f.Close() 60 | graph.Render(chartdraw.PNG, f) 61 | } 62 | -------------------------------------------------------------------------------- /chartdraw/examples/min_max/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/min_max/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | pie := chartdraw.PieChart{ 13 | Width: 512, 14 | Height: 512, 15 | Values: []chartdraw.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(chartdraw.PNG, f) 29 | } 30 | -------------------------------------------------------------------------------- /chartdraw/examples/pie_chart/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/pie_chart/output.png -------------------------------------------------------------------------------- /chartdraw/examples/poly_regression/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate go run main.go 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 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 := chartdraw.ContinuousSeries{ 19 | Name: "A test series", 20 | XValues: chartdraw.Seq{Sequence: chartdraw.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: chartdraw.Seq{Sequence: chartdraw.NewRandomSequence().WithLen(100).WithMin(0).WithMax(100)}.Values(), //generates a []float64 randomly from 0 to 100 with 100 elements. 22 | } 23 | 24 | polyRegSeries := &chartdraw.PolynomialRegressionSeries{ 25 | Degree: 3, 26 | InnerSeries: mainSeries, 27 | } 28 | 29 | graph := chartdraw.Chart{ 30 | Series: []chartdraw.Series{ 31 | mainSeries, 32 | polyRegSeries, 33 | }, 34 | } 35 | 36 | f, _ := os.Create("output.png") 37 | defer f.Close() 38 | graph.Render(chartdraw.PNG, f) 39 | } 40 | -------------------------------------------------------------------------------- /chartdraw/examples/poly_regression/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/poly_regression/output.png -------------------------------------------------------------------------------- /chartdraw/examples/request_timings/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/request_timings/output.png -------------------------------------------------------------------------------- /chartdraw/examples/rerender/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "sync" 7 | "time" 8 | 9 | "github.com/go-analyze/charts/chartdraw" 10 | ) 11 | 12 | var lock sync.Mutex 13 | var graph *chartdraw.Chart 14 | var ts *chartdraw.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, float64(e.Milliseconds())) 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(chartdraw.PNG, res); err != nil { 34 | log.Printf("%v", err) 35 | } 36 | } 37 | 38 | func main() { 39 | ts = &chartdraw.TimeSeries{ 40 | XValues: []time.Time{}, 41 | YValues: []float64{}, 42 | } 43 | graph = &chartdraw.Chart{ 44 | Series: []chartdraw.Series{ts}, 45 | } 46 | http.HandleFunc("/", drawChart) 47 | log.Fatal(http.ListenAndServe(":8080", nil)) 48 | } 49 | -------------------------------------------------------------------------------- /chartdraw/examples/scatter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | _ "net/http/pprof" 7 | 8 | "github.com/go-analyze/charts/chartdraw" 9 | "github.com/go-analyze/charts/chartdraw/drawing" 10 | ) 11 | 12 | func drawChart(res http.ResponseWriter, req *http.Request) { 13 | viridisByY := func(xr, yr chartdraw.Range, index int, x, y float64) drawing.Color { 14 | return chartdraw.Viridis(y, yr.GetMin(), yr.GetMax()) 15 | } 16 | 17 | graph := chartdraw.Chart{ 18 | Series: []chartdraw.Series{ 19 | chartdraw.ContinuousSeries{ 20 | Style: chartdraw.Style{ 21 | StrokeWidth: chartdraw.Disabled, 22 | DotWidth: 5, 23 | DotColorProvider: viridisByY, 24 | }, 25 | XValues: chartdraw.Seq{Sequence: chartdraw.NewLinearSequence().WithStart(0).WithEnd(127)}.Values(), 26 | YValues: chartdraw.Seq{Sequence: chartdraw.NewRandomSequence().WithLen(128).WithMin(0).WithMax(1024)}.Values(), 27 | }, 28 | }, 29 | } 30 | 31 | res.Header().Set("Content-Type", chartdraw.ContentTypePNG) 32 | if err := graph.Render(chartdraw.PNG, res); err != nil { 33 | log.Println(err.Error()) 34 | } 35 | } 36 | 37 | func unit(res http.ResponseWriter, req *http.Request) { 38 | graph := chartdraw.Chart{ 39 | Height: 50, 40 | Width: 50, 41 | Canvas: chartdraw.Style{ 42 | Padding: chartdraw.BoxZero, 43 | }, 44 | Background: chartdraw.Style{ 45 | Padding: chartdraw.BoxZero, 46 | }, 47 | Series: []chartdraw.Series{ 48 | chartdraw.ContinuousSeries{ 49 | XValues: chartdraw.Seq{Sequence: chartdraw.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), 50 | YValues: chartdraw.Seq{Sequence: chartdraw.NewLinearSequence().WithStart(0).WithEnd(4)}.Values(), 51 | }, 52 | }, 53 | } 54 | 55 | res.Header().Set("Content-Type", chartdraw.ContentTypePNG) 56 | if err := graph.Render(chartdraw.PNG, res); err != nil { 57 | log.Println(err.Error()) 58 | } 59 | } 60 | 61 | func main() { 62 | http.HandleFunc("/", drawChart) 63 | http.HandleFunc("/unit", unit) 64 | log.Fatal(http.ListenAndServe(":8080", nil)) 65 | } 66 | -------------------------------------------------------------------------------- /chartdraw/examples/scatter/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/scatter/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | ) 10 | 11 | func main() { 12 | mainSeries := chartdraw.ContinuousSeries{ 13 | Name: "A test series", 14 | XValues: chartdraw.Seq{Sequence: chartdraw.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: chartdraw.Seq{Sequence: chartdraw.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 := &chartdraw.SMASeries{ 21 | InnerSeries: mainSeries, 22 | } // we can optionally set the `WindowSize` property which alters how the moving average is calculated. 23 | 24 | graph := chartdraw.Chart{ 25 | Series: []chartdraw.Series{ 26 | mainSeries, 27 | smaSeries, 28 | }, 29 | } 30 | 31 | f, _ := os.Create("output.png") 32 | defer f.Close() 33 | graph.Render(chartdraw.PNG, f) 34 | } 35 | -------------------------------------------------------------------------------- /chartdraw/examples/simple_moving_average/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/simple_moving_average/output.png -------------------------------------------------------------------------------- /chartdraw/examples/stacked_bar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-analyze/charts/chartdraw" 7 | ) 8 | 9 | func main() { 10 | sbc := chartdraw.StackedBarChart{ 11 | Title: "Test Stacked Bar Chart", 12 | Background: chartdraw.Style{ 13 | Padding: chartdraw.Box{ 14 | Top: 40, 15 | }, 16 | }, 17 | Height: 512, 18 | Bars: []chartdraw.StackedBar{ 19 | { 20 | Name: "This is a very long string to test word break wrapping.", 21 | Values: []chartdraw.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: []chartdraw.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: []chartdraw.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(chartdraw.PNG, f) 53 | } 54 | -------------------------------------------------------------------------------- /chartdraw/examples/stacked_bar/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/stacked_bar/output.png -------------------------------------------------------------------------------- /chartdraw/examples/stacked_bar_labels/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/stacked_bar_labels/output.png -------------------------------------------------------------------------------- /chartdraw/examples/stock_analysis/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/stock_analysis/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 9 | "github.com/go-analyze/charts/chartdraw/drawing" 10 | ) 11 | 12 | func main() { 13 | f := chartdraw.GetDefaultFont() 14 | r := chartdraw.PNG(1024, 1024) 15 | 16 | chartdraw.Draw.Text(r, "Test", 64, 64, chartdraw.Style{ 17 | FontStyle: chartdraw.FontStyle{ 18 | FontColor: drawing.ColorBlack, 19 | FontSize: 18, 20 | Font: f, 21 | }, 22 | }) 23 | 24 | chartdraw.Draw.Text(r, "Test", 64, 64, chartdraw.Style{ 25 | FontStyle: chartdraw.FontStyle{ 26 | FontColor: drawing.ColorBlack, 27 | FontSize: 18, 28 | Font: f, 29 | }, 30 | TextRotationDegrees: 45.0, 31 | }) 32 | 33 | tb := chartdraw.Draw.MeasureText(r, "Test", chartdraw.Style{ 34 | FontStyle: chartdraw.FontStyle{ 35 | FontColor: drawing.ColorBlack, 36 | FontSize: 18, 37 | Font: f, 38 | }, 39 | }).Shift(64, 64) 40 | 41 | tbc := tb.Corners().Rotate(45) 42 | 43 | chartdraw.Draw.BoxCorners(r, tbc, chartdraw.Style{ 44 | StrokeColor: drawing.ColorRed, 45 | StrokeWidth: 2, 46 | }) 47 | 48 | tbcb := tbc.Box() 49 | chartdraw.Draw.Box(r, tbcb, chartdraw.Style{ 50 | StrokeColor: drawing.ColorBlue, 51 | StrokeWidth: 2, 52 | }) 53 | 54 | file, _ := os.Create("output.png") 55 | defer file.Close() 56 | r.Save(file) 57 | } 58 | -------------------------------------------------------------------------------- /chartdraw/examples/text_rotation/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/text_rotation/output.png -------------------------------------------------------------------------------- /chartdraw/examples/timeseries/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/timeseries/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 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 := chartdraw.Chart{ 21 | XAxis: chartdraw.XAxis{ 22 | TickPosition: chartdraw.TickPositionBetweenTicks, 23 | ValueFormatter: func(v interface{}) string { 24 | typed := v.(float64) 25 | typedDate := chartdraw.TimeFromFloat64(typed) 26 | return fmt.Sprintf("%d-%d\n%d", typedDate.Month(), typedDate.Day(), typedDate.Year()) 27 | }, 28 | }, 29 | Series: []chartdraw.Series{ 30 | chartdraw.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 | chartdraw.ContinuousSeries{ 35 | YAxis: chartdraw.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(chartdraw.PNG, f) 45 | } 46 | -------------------------------------------------------------------------------- /chartdraw/examples/twoaxis/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/twoaxis/output.png -------------------------------------------------------------------------------- /chartdraw/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/go-analyze/charts/chartdraw" 11 | ) 12 | 13 | func main() { 14 | var b float64 15 | b = 1000 16 | 17 | ts1 := chartdraw.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 := chartdraw.ContinuousSeries{ //TimeSeries{ 24 | Style: chartdraw.Style{ 25 | StrokeColor: chartdraw.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 := chartdraw.Chart{ 33 | 34 | XAxis: chartdraw.XAxis{ 35 | Name: "The XAxis", 36 | ValueFormatter: chartdraw.TimeMinuteValueFormatter, //TimeHourValueFormatter, 37 | }, 38 | 39 | YAxis: chartdraw.YAxis{ 40 | Name: "The YAxis", 41 | }, 42 | 43 | Series: []chartdraw.Series{ 44 | ts1, 45 | ts2, 46 | }, 47 | } 48 | 49 | buffer := bytes.NewBuffer([]byte{}) 50 | if err := graph.Render(chartdraw.PNG, buffer); err != nil { 51 | log.Fatal(err) 52 | } 53 | if fo, err := os.Create("output.png"); err != nil { 54 | panic(err) 55 | } else if _, err := fo.Write(buffer.Bytes()); err != nil { 56 | panic(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /chartdraw/examples/twopoint/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-analyze/charts/f078cb71019280020d5196675accd33d9535a69d/chartdraw/examples/twopoint/output.png -------------------------------------------------------------------------------- /chartdraw/first_value_annotation.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | // FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation. 4 | func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { 5 | var vf ValueFormatter 6 | if len(vfs) > 0 { 7 | vf = vfs[0] 8 | } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { 9 | _, vf = typed.GetValueFormatters() 10 | } else { 11 | vf = FloatValueFormatter 12 | } 13 | 14 | var firstValue Value2 15 | if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped { 16 | firstValue.XValue, firstValue.YValue = typed.GetFirstValues() 17 | firstValue.Label = vf(firstValue.YValue) 18 | } else { 19 | firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0) 20 | firstValue.Label = vf(firstValue.YValue) 21 | } 22 | 23 | var seriesName string 24 | var seriesStyle Style 25 | if typed, isTyped := innerSeries.(Series); isTyped { 26 | seriesName = typed.GetName() + " - First Value" 27 | seriesStyle = typed.GetStyle() 28 | } 29 | 30 | return AnnotationSeries{ 31 | Name: seriesName, 32 | Style: seriesStyle, 33 | Annotations: []Value2{firstValue}, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chartdraw/first_value_annotation_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFirstValueAnnotation(t *testing.T) { 10 | t.Parallel() 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 | assert.NotEmpty(t, fva.Annotations) 19 | fvaa := fva.Annotations[0] 20 | assert.InDelta(t, float64(1), fvaa.XValue, 0) 21 | assert.InDelta(t, float64(5), fvaa.YValue, 0) 22 | } 23 | -------------------------------------------------------------------------------- /chartdraw/font.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/golang/freetype/truetype" 9 | 10 | "github.com/go-analyze/charts/chartdraw/roboto" 11 | ) 12 | 13 | var fonts = sync.Map{} 14 | var defaultFontFamily = "default" 15 | 16 | func init() { 17 | name := "roboto" 18 | if err := InstallFont(name, roboto.Roboto); err != nil { 19 | panic(fmt.Errorf("could not install default font - %w", err)) 20 | } else if err = SetDefaultFont(name); err != nil { 21 | panic(fmt.Errorf("could not set default font - %w", err)) 22 | } 23 | } 24 | 25 | // InstallFont installs the font for charts 26 | func InstallFont(fontFamily string, data []byte) error { 27 | font, err := truetype.Parse(data) 28 | if err != nil { 29 | return err 30 | } 31 | fonts.Store(fontFamily, font) 32 | return nil 33 | } 34 | 35 | // GetDefaultFont get default font. 36 | func GetDefaultFont() *truetype.Font { 37 | return GetFont(defaultFontFamily) 38 | } 39 | 40 | // SetDefaultFont set default font by name. 41 | func SetDefaultFont(fontFamily string) error { 42 | if value, ok := fonts.Load(fontFamily); ok { 43 | fonts.Store(defaultFontFamily, value) 44 | return nil 45 | } 46 | return errors.New("font not found: " + fontFamily) 47 | } 48 | 49 | // GetFont get the font by font family or the default if the family is not installed. 50 | func GetFont(fontFamily string) *truetype.Font { 51 | if value, ok := fonts.Load(fontFamily); ok { 52 | if f, ok := value.(*truetype.Font); ok { 53 | return f 54 | } 55 | } 56 | return GetDefaultFont() 57 | } 58 | -------------------------------------------------------------------------------- /chartdraw/grid_line.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | if len(ticks) < 3 { 53 | return []GridLine{} 54 | } 55 | 56 | isMinor := false 57 | gl := make([]GridLine, 0, len(ticks)-2) 58 | for _, t := range ticks[1 : len(ticks)-1] { 59 | s := majorStyle 60 | if isMinor { 61 | s = minorStyle 62 | } 63 | gl = append(gl, GridLine{ 64 | Style: s, 65 | IsMinor: isMinor, 66 | Value: t.Value, 67 | }) 68 | isMinor = !isMinor 69 | } 70 | return gl 71 | } 72 | -------------------------------------------------------------------------------- /chartdraw/grid_line_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGenerateGridLines(t *testing.T) { 11 | t.Parallel() 12 | 13 | ticks := []Tick{ 14 | {Value: 1.0, Label: "1.0"}, 15 | {Value: 2.0, Label: "2.0"}, 16 | {Value: 3.0, Label: "3.0"}, 17 | {Value: 4.0, Label: "4.0"}, 18 | } 19 | 20 | gl := GenerateGridLines(ticks, Style{}, Style{}) 21 | require.Len(t, gl, 2) 22 | 23 | assert.InDelta(t, 2.0, gl[0].Value, 0) 24 | assert.InDelta(t, 3.0, gl[1].Value, 0) 25 | } 26 | -------------------------------------------------------------------------------- /chartdraw/histogram_series.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // HistogramSeries is a special type of series that draws as a histogram. 8 | // Some peculiarities; it will always be lower bounded at 0 (at the very least). 9 | // This may alter ranges a bit and generally you want to put a histogram series on its own y-axis. 10 | type HistogramSeries struct { 11 | Name string 12 | Style Style 13 | YAxis YAxisType 14 | InnerSeries ValuesProvider 15 | } 16 | 17 | // GetName implements Series.GetName. 18 | func (hs HistogramSeries) GetName() string { 19 | return hs.Name 20 | } 21 | 22 | // GetStyle implements Series.GetStyle. 23 | func (hs HistogramSeries) GetStyle() Style { 24 | return hs.Style 25 | } 26 | 27 | // GetYAxis returns which yaxis the series is mapped to. 28 | func (hs HistogramSeries) GetYAxis() YAxisType { 29 | return hs.YAxis 30 | } 31 | 32 | // Len implements BoundedValuesProvider.Len. 33 | func (hs HistogramSeries) Len() int { 34 | return hs.InnerSeries.Len() 35 | } 36 | 37 | // GetValues implements ValuesProvider.GetValues. 38 | func (hs HistogramSeries) GetValues(index int) (x, y float64) { 39 | return hs.InnerSeries.GetValues(index) 40 | } 41 | 42 | // GetBoundedValues implements BoundedValuesProvider.GetBoundedValue 43 | func (hs HistogramSeries) GetBoundedValues(index int) (x, y1, y2 float64) { 44 | vx, vy := hs.InnerSeries.GetValues(index) 45 | 46 | x = vx 47 | 48 | if vy > 0 { 49 | y1 = vy 50 | return 51 | } 52 | 53 | y2 = vy 54 | return 55 | } 56 | 57 | // Render implements Series.Render. 58 | func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { 59 | style := hs.Style.InheritFrom(defaults) 60 | Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs) 61 | } 62 | 63 | // Validate validates the series. 64 | func (hs HistogramSeries) Validate() error { 65 | if hs.InnerSeries == nil { 66 | return errors.New("histogram series requires InnerSeries to be set") 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /chartdraw/histogram_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestHistogramSeries(t *testing.T) { 10 | t.Parallel() 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 | assert.InDelta(t, csx, hsx, 0) 26 | assert.Positive(t, hsy1) 27 | assert.LessOrEqual(t, hsy2, 0.0) 28 | assert.True(t, csy < 0 || (csy > 0 && csy == hsy1)) 29 | assert.True(t, csy > 0 || (csy < 0 && csy == hsy2)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chartdraw/image_writer.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/jet.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "github.com/go-analyze/charts/chartdraw/drawing" 5 | ) 6 | 7 | // Jet is a color map provider based on matlab's jet color map. 8 | func Jet(v, vmin, vmax float64) drawing.Color { 9 | c := drawing.Color{R: 0xff, G: 0xff, B: 0xff, A: 0xff} // white 10 | var dv float64 11 | 12 | if v < vmin { 13 | v = vmin 14 | } 15 | if v > vmax { 16 | v = vmax 17 | } 18 | dv = vmax - vmin 19 | 20 | if v < (vmin + 0.25*dv) { 21 | c.R = 0 22 | c.G = drawing.ColorChannelFromFloat(4 * (v - vmin) / dv) 23 | } else if v < (vmin + 0.5*dv) { 24 | c.R = 0 25 | c.B = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.25*dv-v)/dv) 26 | } else if v < (vmin + 0.75*dv) { 27 | c.R = drawing.ColorChannelFromFloat(4 * (v - vmin - 0.5*dv) / dv) 28 | c.B = 0 29 | } else { 30 | c.G = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.75*dv-v)/dv) 31 | c.B = 0 32 | } 33 | 34 | return c 35 | } 36 | -------------------------------------------------------------------------------- /chartdraw/last_value_annotation_series.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | // LastValueAnnotationSeries returns an annotation series of just the last value of a value provider. 4 | func LastValueAnnotationSeries(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { 5 | var vf ValueFormatter 6 | if len(vfs) > 0 { 7 | vf = vfs[0] 8 | } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped { 9 | _, vf = typed.GetValueFormatters() 10 | } else { 11 | vf = FloatValueFormatter 12 | } 13 | 14 | var lastValue Value2 15 | if typed, isTyped := innerSeries.(LastValuesProvider); isTyped { 16 | lastValue.XValue, lastValue.YValue = typed.GetLastValues() 17 | lastValue.Label = vf(lastValue.YValue) 18 | } else { 19 | lastValue.XValue, lastValue.YValue = innerSeries.GetValues(innerSeries.Len() - 1) 20 | lastValue.Label = vf(lastValue.YValue) 21 | } 22 | 23 | var seriesName string 24 | var seriesStyle Style 25 | if typed, isTyped := innerSeries.(Series); isTyped { 26 | seriesName = typed.GetName() + " - Last Value" 27 | seriesStyle = typed.GetStyle() 28 | } 29 | 30 | return AnnotationSeries{ 31 | Name: seriesName, 32 | Style: seriesStyle, 33 | Annotations: []Value2{lastValue}, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chartdraw/last_value_annotation_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLastValueAnnotationSeries(t *testing.T) { 10 | t.Parallel() 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 | assert.NotEmpty(t, lva.Annotations) 19 | lvaa := lva.Annotations[0] 20 | assert.InDelta(t, float64(5), lvaa.XValue, 0) 21 | assert.InDelta(t, float64(1), lvaa.YValue, 0) 22 | } 23 | -------------------------------------------------------------------------------- /chartdraw/legend_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestLegend(t *testing.T) { 12 | t.Parallel() 13 | 14 | graph := Chart{ 15 | Series: []Series{ 16 | ContinuousSeries{ 17 | Name: "A test series", 18 | XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 19 | YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, 20 | }, 21 | }, 22 | } 23 | 24 | //note we have to do this as a separate step because we need a reference to graph 25 | graph.Elements = []Renderable{ 26 | Legend(&graph), 27 | } 28 | buf := bytes.NewBuffer([]byte{}) 29 | require.NoError(t, graph.Render(PNG, buf)) 30 | assert.NotZero(t, buf.Len()) 31 | } 32 | -------------------------------------------------------------------------------- /chartdraw/linear_coefficient_provider.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | // LinearCoefficientProvider is a type that returns linear coefficients. 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 | -------------------------------------------------------------------------------- /chartdraw/linear_regression_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLinearRegressionSeries(t *testing.T) { 10 | t.Parallel() 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 | assert.InDelta(t, 1.0, lrx0, 0.0000001) 24 | assert.InDelta(t, 1.0, lry0, 0.0000001) 25 | 26 | lrxn, lryn := linRegSeries.GetLastValues() 27 | assert.InDelta(t, 100.0, lrxn, 0.0000001) 28 | assert.InDelta(t, 100.0, lryn, 0.0000001) 29 | } 30 | 31 | func TestLinearRegressionSeriesDesc(t *testing.T) { 32 | t.Parallel() 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 | assert.InDelta(t, 100.0, lrx0, 0.0000001) 46 | assert.InDelta(t, 100.0, lry0, 0.0000001) 47 | 48 | lrxn, lryn := linRegSeries.GetLastValues() 49 | assert.InDelta(t, 1.0, lrxn, 0.0000001) 50 | assert.InDelta(t, 1.0, lryn, 0.0000001) 51 | } 52 | 53 | func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { 54 | t.Parallel() 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 | assert.Equal(t, 10, linRegSeries.Len()) 69 | 70 | lrx0, lry0 := linRegSeries.GetValues(0) 71 | assert.InDelta(t, 90.0, lrx0, 0.0000001) 72 | assert.InDelta(t, 90.0, lry0, 0.0000001) 73 | 74 | lrxn, lryn := linRegSeries.GetLastValues() 75 | assert.InDelta(t, 80.0, lrxn, 0.0000001) 76 | assert.InDelta(t, 80.0, lryn, 0.0000001) 77 | } 78 | -------------------------------------------------------------------------------- /chartdraw/linear_sequence.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/logarithmic_range_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLogRangeTranslate(t *testing.T) { 11 | t.Parallel() 12 | 13 | values := []float64{1, 10, 100, 1000, 10000, 100000, 1000000} 14 | r := LogarithmicRange{Domain: 1000} 15 | r.Min, r.Max = MinMax(values...) 16 | 17 | assert.Equal(t, 0, r.Translate(0)) // goes to bottom 18 | assert.Equal(t, 0, r.Translate(1)) // goes to bottom 19 | assert.Equal(t, 160, r.Translate(10)) // roughly 1/6th of max 20 | assert.Equal(t, 500, r.Translate(1000)) // roughly 1/2 of max (1.0e6 / 1.0e3) 21 | assert.Equal(t, 1000, r.Translate(1000000)) // max value 22 | } 23 | 24 | func TestGetTicks(t *testing.T) { 25 | t.Parallel() 26 | 27 | values := []float64{35, 512, 1525122} 28 | r := LogarithmicRange{Domain: 1000} 29 | r.Min, r.Max = MinMax(values...) 30 | 31 | ticks := r.GetTicks(FloatValueFormatter) 32 | require.Len(t, ticks, 7) 33 | assert.InDelta(t, float64(10), ticks[0].Value, 0) 34 | assert.InDelta(t, float64(100), ticks[1].Value, 0) 35 | assert.InDelta(t, float64(10000000), ticks[6].Value, 0) 36 | } 37 | 38 | func TestGetTicksFromHigh(t *testing.T) { 39 | t.Parallel() 40 | 41 | values := []float64{1412, 352144, 1525122} // min tick should be 1000 42 | r := LogarithmicRange{} 43 | r.Min, r.Max = MinMax(values...) 44 | 45 | ticks := r.GetTicks(FloatValueFormatter) 46 | require.Len(t, ticks, 5) 47 | assert.InDelta(t, float64(1000), ticks[0].Value, 0) 48 | assert.InDelta(t, float64(10000), ticks[1].Value, 0) 49 | assert.InDelta(t, float64(10000000), ticks[4].Value, 0) 50 | } 51 | -------------------------------------------------------------------------------- /chartdraw/macd_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 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 | t.Parallel() 67 | 68 | mockSeries := mockValuesProvider{ 69 | emaXValues, 70 | emaYValues, 71 | } 72 | assert.Equal(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 | assert.NotEmpty(t, yvalues) 85 | require.Len(t, yvalues, len(macdExpected)) 86 | for index, vy := range yvalues { 87 | assert.InDelta(t, macdExpected[index], vy, emaDelta) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /chartdraw/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 | -------------------------------------------------------------------------------- /chartdraw/matrix/regression_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestPoly(t *testing.T) { 11 | t.Parallel() 12 | 13 | var xGiven = []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 14 | var yGiven = []float64{1, 6, 17, 34, 57, 86, 121, 162, 209, 262, 321} 15 | var degree = 2 16 | 17 | c, err := Poly(xGiven, yGiven, degree) 18 | require.NoError(t, err) 19 | assert.Len(t, c, 3) 20 | 21 | assert.InDelta(t, 0.999999999, c[0], DefaultEpsilon) 22 | assert.InDelta(t, 2, c[1], DefaultEpsilon) 23 | assert.InDelta(t, 3, c[2], DefaultEpsilon) 24 | } 25 | -------------------------------------------------------------------------------- /chartdraw/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 | for x := 0; x < len(values); x++ { 11 | if values[x] < min { 12 | min = values[x] 13 | } 14 | } 15 | return min 16 | } 17 | 18 | func f64s(v float64) string { 19 | return strconv.FormatFloat(v, 'f', -1, 64) 20 | } 21 | 22 | func roundToEpsilon(value, epsilon float64) float64 { 23 | // TODO - epsilon is not used here, this does not appear to be as the function describes 24 | return math.Nextafter(value, value) 25 | } 26 | -------------------------------------------------------------------------------- /chartdraw/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 += v[i] * v2[i] 15 | } 16 | return 17 | } 18 | -------------------------------------------------------------------------------- /chartdraw/matrix/vector_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestVectorDotProduct(t *testing.T) { 11 | t.Parallel() 12 | 13 | v1 := Vector{1, 2, 3} 14 | v2 := Vector{4, 5, 6} 15 | 16 | result, err := v1.DotProduct(v2) 17 | require.NoError(t, err) 18 | assert.InDelta(t, float64(32), result, 0) 19 | } 20 | 21 | func TestVectorDotProductDimensionMismatch(t *testing.T) { 22 | t.Parallel() 23 | 24 | _, err := Vector{1, 2}.DotProduct(Vector{1}) 25 | assert.ErrorIs(t, err, ErrDimensionMismatch) 26 | } 27 | -------------------------------------------------------------------------------- /chartdraw/percent_change_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPercentageDifferenceSeries(t *testing.T) { 10 | t.Parallel() 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 | assert.Equal(t, "Test Series", pcs.GetName()) 23 | assert.Equal(t, 10, pcs.Len()) 24 | x0, y0 := pcs.GetValues(0) 25 | assert.InDelta(t, 1.0, x0, 0) 26 | assert.InDelta(t, 0.0, y0, 0) 27 | 28 | xn, yn := pcs.GetValues(9) 29 | assert.InDelta(t, 10.0, xn, 0) 30 | assert.InDelta(t, 9.0, yn, 0) 31 | 32 | xn, yn = pcs.GetLastValues() 33 | assert.InDelta(t, 10.0, xn, 0) 34 | assert.InDelta(t, 9.0, yn, 0) 35 | } 36 | -------------------------------------------------------------------------------- /chartdraw/pie_chart_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPieChart(t *testing.T) { 12 | t.Parallel() 13 | 14 | pie := PieChart{ 15 | Canvas: Style{ 16 | FillColor: ColorLightGray, 17 | }, 18 | Values: []Value{ 19 | {Value: 10, Label: "Blue"}, 20 | {Value: 9, Label: "Green"}, 21 | {Value: 8, Label: "Gray"}, 22 | {Value: 7, Label: "Orange"}, 23 | {Value: 6, Label: "HEANG"}, 24 | {Value: 5, Label: "??"}, 25 | {Value: 2, Label: "!!"}, 26 | }, 27 | } 28 | 29 | b := bytes.NewBuffer([]byte{}) 30 | require.NoError(t, pie.Render(PNG, b)) 31 | assert.NotZero(t, b.Len()) 32 | } 33 | 34 | func TestPieChartDropsZeroValues(t *testing.T) { 35 | t.Parallel() 36 | 37 | pie := PieChart{ 38 | Canvas: Style{ 39 | FillColor: ColorLightGray, 40 | }, 41 | Values: []Value{ 42 | {Value: 5, Label: "Blue"}, 43 | {Value: 5, Label: "Green"}, 44 | {Value: 0, Label: "Gray"}, 45 | }, 46 | } 47 | 48 | b := bytes.NewBuffer([]byte{}) 49 | require.NoError(t, pie.Render(PNG, b)) 50 | } 51 | 52 | func TestPieChartAllZeroValues(t *testing.T) { 53 | t.Parallel() 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 | require.Error(t, pie.Render(PNG, b)) 68 | } 69 | -------------------------------------------------------------------------------- /chartdraw/polynomial_regression_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/go-analyze/charts/chartdraw/matrix" 9 | ) 10 | 11 | func TestPolynomialRegression(t *testing.T) { 12 | t.Parallel() 13 | 14 | var xv []float64 15 | var yv []float64 16 | 17 | for i := 0; i < 100; i++ { 18 | xv = append(xv, float64(i)) 19 | yv = append(yv, float64(i*i)) 20 | } 21 | 22 | values := ContinuousSeries{ 23 | XValues: xv, 24 | YValues: yv, 25 | } 26 | 27 | poly := &PolynomialRegressionSeries{ 28 | InnerSeries: values, 29 | Degree: 2, 30 | } 31 | 32 | for i := 0; i < 100; i++ { 33 | _, y := poly.GetValues(i) 34 | assert.InDelta(t, float64(i*i), y, matrix.DefaultEpsilon) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /chartdraw/random_sequence.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/range.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/raster_renderer_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/go-analyze/charts/chartdraw/drawing" 8 | ) 9 | 10 | func BenchmarkRaterCircle(b *testing.B) { 11 | testRadius := []float64{400, 200, 128, 64, 16, 8, 2} 12 | bb := &bytes.Buffer{} 13 | for i := 0; i < b.N; i++ { 14 | png := PNG(800, 800) 15 | jpg := JPG(800, 800) 16 | 17 | var flip bool 18 | for _, r := range testRadius { 19 | color := drawing.ColorNavy 20 | if flip { 21 | color = drawing.ColorThistle 22 | flip = false 23 | } else { 24 | flip = true 25 | } 26 | 27 | png.SetFillColor(color) 28 | png.Circle(r, 400, 400) 29 | png.Fill() 30 | 31 | jpg.SetFillColor(color) 32 | jpg.Circle(r, 400, 400) 33 | jpg.Fill() 34 | } 35 | 36 | bb.Reset() 37 | _ = png.Save(bb) 38 | bb.Reset() 39 | _ = jpg.Save(bb) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /chartdraw/renderable.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/renderer_provider.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | // RendererProvider is a function that returns a renderer. 4 | type RendererProvider func(int, int) Renderer 5 | -------------------------------------------------------------------------------- /chartdraw/series.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 | -------------------------------------------------------------------------------- /chartdraw/tick_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGenerateContinuousTicks(t *testing.T) { 11 | t.Parallel() 12 | 13 | r := PNG(1024, 1024) 14 | r.SetFont(GetDefaultFont()) 15 | 16 | ra := &ContinuousRange{ 17 | Min: 0.0, 18 | Max: 10.0, 19 | Domain: 256, 20 | } 21 | 22 | vf := FloatValueFormatter 23 | 24 | ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) 25 | assert.NotEmpty(t, ticks) 26 | require.Len(t, ticks, 11) 27 | assert.InDelta(t, 0.0, ticks[0].Value, 0) 28 | assert.InDelta(t, 10.0, ticks[len(ticks)-1].Value, 0) 29 | } 30 | 31 | func TestGenerateContinuousTicksDescending(t *testing.T) { 32 | t.Parallel() 33 | 34 | r := PNG(1024, 1024) 35 | r.SetFont(GetDefaultFont()) 36 | 37 | ra := &ContinuousRange{ 38 | Min: 0.0, 39 | Max: 10.0, 40 | Domain: 256, 41 | Descending: true, 42 | } 43 | 44 | vf := FloatValueFormatter 45 | 46 | ticks := GenerateContinuousTicks(r, ra, false, Style{}, vf) 47 | assert.NotEmpty(t, ticks) 48 | require.Len(t, ticks, 11) 49 | assert.InDelta(t, 10.0, ticks[0].Value, 0) 50 | assert.InDelta(t, 9.0, ticks[1].Value, 0) 51 | assert.InDelta(t, 1.0, ticks[len(ticks)-2].Value, 0) 52 | assert.InDelta(t, 0.0, ticks[len(ticks)-1].Value, 0) 53 | } 54 | -------------------------------------------------------------------------------- /chartdraw/time_series_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTimeSeriesGetValue(t *testing.T) { 12 | t.Parallel() 13 | 14 | now := time.Now() 15 | ts := TimeSeries{ 16 | Name: "Test", 17 | XValues: []time.Time{ 18 | now.AddDate(0, 0, -5), 19 | now.AddDate(0, 0, -4), 20 | now.AddDate(0, 0, -3), 21 | now.AddDate(0, 0, -2), 22 | now.AddDate(0, 0, -1), 23 | }, 24 | YValues: []float64{ 25 | 1.0, 2.0, 3.0, 4.0, 5.0, 26 | }, 27 | } 28 | 29 | x0, y0 := ts.GetValues(0) 30 | assert.NotZero(t, x0) 31 | assert.InDelta(t, 1.0, y0, 0) 32 | } 33 | 34 | func TestTimeSeriesValidate(t *testing.T) { 35 | t.Parallel() 36 | 37 | now := time.Now() 38 | cs := TimeSeries{ 39 | Name: "Test Series", 40 | XValues: []time.Time{ 41 | now.AddDate(0, 0, -5), 42 | now.AddDate(0, 0, -4), 43 | now.AddDate(0, 0, -3), 44 | now.AddDate(0, 0, -2), 45 | now.AddDate(0, 0, -1), 46 | }, 47 | YValues: []float64{ 48 | 1.0, 2.0, 3.0, 4.0, 5.0, 49 | }, 50 | } 51 | require.NoError(t, cs.Validate()) 52 | 53 | cs = TimeSeries{ 54 | Name: "Test Series", 55 | XValues: []time.Time{ 56 | now.AddDate(0, 0, -5), 57 | now.AddDate(0, 0, -4), 58 | now.AddDate(0, 0, -3), 59 | now.AddDate(0, 0, -2), 60 | now.AddDate(0, 0, -1), 61 | }, 62 | } 63 | require.Error(t, cs.Validate()) 64 | 65 | cs = TimeSeries{ 66 | Name: "Test Series", 67 | YValues: []float64{ 68 | 1.0, 2.0, 3.0, 4.0, 5.0, 69 | }, 70 | } 71 | require.Error(t, cs.Validate()) 72 | } 73 | -------------------------------------------------------------------------------- /chartdraw/times.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 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 | -------------------------------------------------------------------------------- /chartdraw/timeutil.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import "time" 4 | 5 | // TimeToFloat64 returns a float64 representation of a time. 6 | func TimeToFloat64(t time.Time) float64 { 7 | if t.IsZero() { 8 | return 0 9 | } 10 | return float64(t.UnixNano()) 11 | } 12 | 13 | // TimeFromFloat64 returns a time in nanosecond from a float64. 14 | func TimeFromFloat64(tf float64) time.Time { 15 | if tf == 0 { 16 | return time.Time{} 17 | } 18 | return time.Unix(0, int64(tf)) 19 | } 20 | -------------------------------------------------------------------------------- /chartdraw/timeutil_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestTimeToFloat64(t *testing.T) { 9 | // zero time 10 | tf := TimeToFloat64(time.Time{}) 11 | if tf != 0 { 12 | t.Errorf("Expected float64 representation of zero time to be 0, but got %f", tf) 13 | } 14 | 15 | // non-zero time 16 | tm := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) 17 | expectedTF := float64(tm.UnixNano()) 18 | tf = TimeToFloat64(tm) 19 | if tf != expectedTF { 20 | t.Errorf("Expected float64 representation of time %s to be %f, but got %f", tm, expectedTF, tf) 21 | } 22 | } 23 | 24 | func TestTimeFromFloat64(t *testing.T) { 25 | // zero float64 26 | expectedT := time.Time{} 27 | actualT := TimeFromFloat64(0) 28 | if actualT != expectedT { 29 | t.Errorf("Expected time from float64 representation of 0 to be zero time, but got %s", actualT) 30 | } 31 | 32 | // non-zero float64 represent nanoseconds 33 | expectedT = time.Date(2022, 1, 1, 0, 0, 0, 123456789, time.Local) 34 | nanosecondsFloat := float64(expectedT.UnixNano()) 35 | actualT = TimeFromFloat64(nanosecondsFloat) 36 | if actualT.Equal(expectedT) { 37 | t.Errorf("Expected time from float64 representation %f to be %s, but got %s", nanosecondsFloat, expectedT, actualT) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chartdraw/value.go: -------------------------------------------------------------------------------- 1 | package chartdraw 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 total float64 30 | for _, v := range vs { 31 | total += v.Value 32 | } 33 | 34 | output := make([]Value, 0, len(vs)) 35 | for _, v := range vs { 36 | if v.Value > 0 { 37 | output = append(output, Value{ 38 | Style: v.Style, 39 | Label: v.Label, 40 | Value: RoundDown(v.Value/total, 0.0001), 41 | }) 42 | } 43 | } 44 | return output 45 | } 46 | 47 | // Value2 is a two axis value. 48 | type Value2 struct { 49 | Style Style 50 | Label string 51 | XValue, YValue float64 52 | } 53 | -------------------------------------------------------------------------------- /chartdraw/value_formatter_provider.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | // ValueFormatterProvider is a series that has custom formatters. 4 | type ValueFormatterProvider interface { 5 | GetValueFormatters() (x, y ValueFormatter) 6 | } 7 | -------------------------------------------------------------------------------- /chartdraw/value_formatter_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTimeValueFormatterWithFormat(t *testing.T) { 12 | t.Parallel() 13 | 14 | d := time.Now() 15 | df := TimeToFloat64(d) 16 | 17 | s := formatTime(d, DefaultDateFormat) 18 | sf := formatTime(df, DefaultDateFormat) 19 | assert.Equal(t, s, sf) 20 | 21 | sd := TimeValueFormatter(d) 22 | sdf := TimeValueFormatter(df) 23 | assert.Equal(t, s, sd) 24 | assert.Equal(t, s, sdf) 25 | } 26 | 27 | func TestFloatValueFormatter(t *testing.T) { 28 | t.Parallel() 29 | 30 | testCases := []struct { 31 | name string 32 | input interface{} 33 | }{ 34 | { 35 | name: "basic_float", 36 | input: 1234.00, 37 | }, 38 | { 39 | name: "float32", 40 | input: float32(1234.00), 41 | }, 42 | { 43 | name: "int", 44 | input: 1234, 45 | }, 46 | { 47 | name: "int64", 48 | input: int64(1234), 49 | }, 50 | } 51 | 52 | for i, tc := range testCases { 53 | t.Run(strconv.Itoa(i)+"-"+tc.name, func(t *testing.T) { 54 | assert.Equal(t, "1234.00", FloatValueFormatter(tc.input)) 55 | }) 56 | } 57 | } 58 | 59 | func TestFloatValueFormatterWithFormat(t *testing.T) { 60 | t.Parallel() 61 | 62 | v := 123.456 63 | sv := FloatValueFormatterWithFormat(v, "%.3f") 64 | assert.Equal(t, "123.456", sv) 65 | assert.Equal(t, "123.000", FloatValueFormatterWithFormat(123, "%.3f")) 66 | } 67 | 68 | func TestExponentialValueFormatter(t *testing.T) { 69 | t.Parallel() 70 | 71 | assert.Equal(t, "1.23e+02", ExponentialValueFormatter(123.456)) 72 | assert.Equal(t, "1.24e+07", ExponentialValueFormatter(12421243.424)) 73 | assert.Equal(t, "4.50e-01", ExponentialValueFormatter(0.45)) 74 | } 75 | -------------------------------------------------------------------------------- /chartdraw/value_provider.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "github.com/go-analyze/charts/chartdraw/drawing" 5 | ) 6 | 7 | // ValuesProvider is a type that produces values. 8 | type ValuesProvider interface { 9 | Len() int 10 | GetValues(index int) (float64, float64) 11 | } 12 | 13 | // BoundedValuesProvider allows series to return a range. 14 | type BoundedValuesProvider interface { 15 | Len() int 16 | GetBoundedValues(index int) (x, y1, y2 float64) 17 | } 18 | 19 | // FirstValuesProvider is a special type of value provider that can return it's (potentially computed) first value. 20 | type FirstValuesProvider interface { 21 | GetFirstValues() (x, y float64) 22 | } 23 | 24 | // LastValuesProvider is a special type of value provider that can return it's (potentially computed) last value. 25 | type LastValuesProvider interface { 26 | GetLastValues() (x, y float64) 27 | } 28 | 29 | // BoundedLastValuesProvider is a special type of value provider that can return it's (potentially computed) bounded last value. 30 | type BoundedLastValuesProvider interface { 31 | GetBoundedLastValues() (x, y1, y2 float64) 32 | } 33 | 34 | // FullValuesProvider is an interface that combines `ValuesProvider` and `LastValuesProvider` 35 | type FullValuesProvider interface { 36 | ValuesProvider 37 | LastValuesProvider 38 | } 39 | 40 | // FullBoundedValuesProvider is an interface that combines `BoundedValuesProvider` and `BoundedLastValuesProvider` 41 | type FullBoundedValuesProvider interface { 42 | BoundedValuesProvider 43 | BoundedLastValuesProvider 44 | } 45 | 46 | // SizeProvider is a provider for integer size. 47 | type SizeProvider func(xrange, yrange Range, index int, x, y float64) float64 48 | 49 | // ColorProvider is a general provider for color ranges based on values. 50 | type ColorProvider func(v, vmin, vmax float64) drawing.Color 51 | 52 | // DotColorProvider is a provider for dot color. 53 | type DotColorProvider func(xrange, yrange Range, index int, x, y float64) drawing.Color 54 | -------------------------------------------------------------------------------- /chartdraw/value_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestValuesValues(t *testing.T) { 11 | t.Parallel() 12 | 13 | vs := []Value{ 14 | {Value: 10, Label: "Blue"}, 15 | {Value: 9, Label: "Green"}, 16 | {Value: 8, Label: "Gray"}, 17 | {Value: 7, Label: "Orange"}, 18 | {Value: 6, Label: "HEANG"}, 19 | {Value: 5, Label: "??"}, 20 | {Value: 2, Label: "!!"}, 21 | } 22 | 23 | values := Values(vs).Values() 24 | require.Len(t, values, 7) 25 | assert.InDelta(t, float64(10), values[0], 0) 26 | assert.InDelta(t, float64(9), values[1], 0) 27 | assert.InDelta(t, float64(8), values[2], 0) 28 | assert.InDelta(t, float64(7), values[3], 0) 29 | assert.InDelta(t, float64(6), values[4], 0) 30 | assert.InDelta(t, float64(5), values[5], 0) 31 | assert.InDelta(t, float64(2), values[6], 0) 32 | } 33 | 34 | func TestValuesValuesNormalized(t *testing.T) { 35 | t.Parallel() 36 | 37 | vs := []Value{ 38 | {Value: 10, Label: "Blue"}, 39 | {Value: 9, Label: "Green"}, 40 | {Value: 8, Label: "Gray"}, 41 | {Value: 7, Label: "Orange"}, 42 | {Value: 6, Label: "HEANG"}, 43 | {Value: 5, Label: "??"}, 44 | {Value: 2, Label: "!!"}, 45 | } 46 | 47 | values := Values(vs).ValuesNormalized() 48 | require.Len(t, values, 7) 49 | assert.InDelta(t, 0.2127, values[0], 0) 50 | assert.InDelta(t, 0.0425, values[6], 0) 51 | } 52 | 53 | func TestValuesNormalize(t *testing.T) { 54 | t.Parallel() 55 | 56 | vs := []Value{ 57 | {Value: 10, Label: "Blue"}, 58 | {Value: 9, Label: "Green"}, 59 | {Value: 8, Label: "Gray"}, 60 | {Value: 7, Label: "Orange"}, 61 | {Value: 6, Label: "HEANG"}, 62 | {Value: 5, Label: "??"}, 63 | {Value: 2, Label: "!!"}, 64 | } 65 | 66 | values := Values(vs).Normalize() 67 | require.Len(t, values, 7) 68 | assert.InDelta(t, 0.2127, values[0].Value, 0) 69 | assert.InDelta(t, 0.0425, values[6].Value, 0) 70 | } 71 | -------------------------------------------------------------------------------- /chartdraw/xaxis_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestXAxisGetTicks(t *testing.T) { 10 | t.Parallel() 11 | 12 | r := PNG(1024, 1024) 13 | 14 | xa := XAxis{} 15 | xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 16 | styleDefaults := Style{ 17 | FontStyle: FontStyle{ 18 | Font: GetDefaultFont(), 19 | FontSize: 10.0, 20 | }, 21 | } 22 | vf := FloatValueFormatter 23 | ticks := xa.GetTicks(r, xr, styleDefaults, vf) 24 | assert.Len(t, ticks, 16) 25 | } 26 | 27 | func TestXAxisGetTicksWithUserDefaults(t *testing.T) { 28 | t.Parallel() 29 | 30 | r := PNG(1024, 1024) 31 | 32 | xa := XAxis{ 33 | Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, 34 | } 35 | xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 36 | styleDefaults := Style{ 37 | FontStyle: FontStyle{ 38 | Font: GetDefaultFont(), 39 | FontSize: 10.0, 40 | }, 41 | } 42 | vf := FloatValueFormatter 43 | ticks := xa.GetTicks(r, xr, styleDefaults, vf) 44 | assert.Len(t, ticks, 1) 45 | } 46 | 47 | func TestXAxisMeasure(t *testing.T) { 48 | t.Parallel() 49 | 50 | style := Style{ 51 | FontStyle: FontStyle{ 52 | Font: GetDefaultFont(), 53 | FontSize: 10.0, 54 | }, 55 | } 56 | r := PNG(100, 100) 57 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 58 | xa := XAxis{} 59 | xab := xa.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 60 | assert.Equal(t, 122, xab.Width()) 61 | assert.Equal(t, 21, xab.Height()) 62 | } 63 | -------------------------------------------------------------------------------- /chartdraw/yaxis_test.go: -------------------------------------------------------------------------------- 1 | package chartdraw 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestYAxisGetTicks(t *testing.T) { 10 | t.Parallel() 11 | 12 | r := PNG(1024, 1024) 13 | 14 | ya := YAxis{} 15 | yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 16 | styleDefaults := Style{ 17 | FontStyle: FontStyle{ 18 | Font: GetDefaultFont(), 19 | FontSize: 10.0, 20 | }, 21 | } 22 | vf := FloatValueFormatter 23 | ticks := ya.GetTicks(r, yr, styleDefaults, vf) 24 | assert.Len(t, ticks, 32) 25 | } 26 | 27 | func TestYAxisGetTicksWithUserDefaults(t *testing.T) { 28 | t.Parallel() 29 | 30 | r := PNG(1024, 1024) 31 | 32 | ya := YAxis{ 33 | Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, 34 | } 35 | yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} 36 | styleDefaults := Style{ 37 | FontStyle: FontStyle{ 38 | Font: GetDefaultFont(), 39 | FontSize: 10.0, 40 | }, 41 | } 42 | vf := FloatValueFormatter 43 | ticks := ya.GetTicks(r, yr, styleDefaults, vf) 44 | assert.Len(t, ticks, 1) 45 | } 46 | 47 | func TestYAxisMeasure(t *testing.T) { 48 | t.Parallel() 49 | 50 | style := Style{ 51 | FontStyle: FontStyle{ 52 | Font: GetDefaultFont(), 53 | FontSize: 10.0, 54 | }, 55 | } 56 | r := PNG(100, 100) 57 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 58 | ya := YAxis{} 59 | yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 60 | assert.Equal(t, 32, yab.Width()) 61 | assert.Equal(t, 110, yab.Height()) 62 | } 63 | 64 | func TestYAxisSecondaryMeasure(t *testing.T) { 65 | t.Parallel() 66 | 67 | style := Style{ 68 | FontStyle: FontStyle{ 69 | Font: GetDefaultFont(), 70 | FontSize: 10.0, 71 | }, 72 | } 73 | r := PNG(100, 100) 74 | ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}} 75 | ya := YAxis{AxisType: YAxisSecondary} 76 | yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks) 77 | assert.Equal(t, 32, yab.Width()) 78 | assert.Equal(t, 110, yab.Height()) 79 | } 80 | -------------------------------------------------------------------------------- /examples/1-Painter/bar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "bar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3}, 27 | {2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3}, 28 | } 29 | 30 | opt := charts.NewBarChartOptionWithData(values) 31 | opt.Title.Text = "Bar Chart" 32 | opt.XAxis.Labels = []string{ 33 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 34 | } 35 | opt.Legend = charts.LegendOption{ 36 | SeriesNames: []string{ 37 | "Rainfall", "Evaporation", 38 | }, 39 | Offset: charts.OffsetRight, 40 | OverlayChart: charts.Ptr(true), 41 | } 42 | 43 | p := charts.NewPainter(charts.PainterOptions{ 44 | OutputFormat: charts.ChartOutputPNG, 45 | Width: 600, 46 | Height: 400, 47 | }) 48 | if err := p.BarChart(opt); err != nil { 49 | panic(err) 50 | } else if buf, err := p.Bytes(); err != nil { 51 | panic(err) 52 | } else if err = writeFile(buf); err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/1-Painter/bar_chart-2-size_margin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart with custom bar sizes and margins. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "bar-chart-2-size_margin.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {2.0, 4.9, 7.0, 23.2, 25.6, 76.7}, 27 | {2.6, 5.9, 9.0, 26.4, 28.7, 70.7}, 28 | } 29 | 30 | opt := charts.NewBarChartOptionWithData(values) 31 | opt.XAxis.Labels = []string{ 32 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 33 | } 34 | opt.Legend.Show = charts.Ptr(false) 35 | 36 | p := charts.NewPainter(charts.PainterOptions{ 37 | OutputFormat: charts.ChartOutputPNG, 38 | Width: 1200, 39 | Height: 400, 40 | }) 41 | defaultPainter := p.Child(charts.PainterBoxOption(charts.NewBox(0, 0, 400, 400))) 42 | opt.Title.Text = "Default" 43 | if err := defaultPainter.BarChart(opt); err != nil { 44 | panic(err) 45 | } 46 | barSizePainter := p.Child(charts.PainterBoxOption(charts.NewBox(400, 0, 800, 400))) 47 | opt.Title.Text = "Small Bar" 48 | opt.BarWidth = 4 49 | if err := barSizePainter.BarChart(opt); err != nil { 50 | panic(err) 51 | } 52 | marginPainter := p.Child(charts.PainterBoxOption(charts.NewBox(800, 0, 1200, 400))) 53 | opt.Title.Text = "No Margin" 54 | opt.BarMargin = charts.Ptr(0.0) 55 | opt.BarWidth = 0 // reset to default size 56 | if err := marginPainter.BarChart(opt); err != nil { 57 | panic(err) 58 | } 59 | 60 | if buf, err := p.Bytes(); err != nil { 61 | panic(err) 62 | } else if err = writeFile(buf); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/1-Painter/bar_chart-3-label_position-round_caps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart showing different series label positions and with rounded caps. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "bar-chart-3-label_position.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {23.2, 25.6, 76.7, 135.6, 162.2, 32.6}, 27 | {26.4, 28.7, 70.7, 175.6, 182.2, 48.7}, 28 | } 29 | 30 | opt := charts.NewBarChartOptionWithData(values) 31 | opt.XAxis.Labels = []string{ 32 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 33 | } 34 | opt.BarMargin = charts.Ptr(1.0) 35 | for i := range opt.SeriesList { 36 | opt.SeriesList[i].Label.Show = charts.Ptr(true) 37 | opt.SeriesList[i].Label.ValueFormatter = func(f float64) string { 38 | return charts.FormatValueHumanizeShort(f, 0, false) 39 | } 40 | } 41 | opt.RoundedBarCaps = charts.Ptr(true) 42 | 43 | p := charts.NewPainter(charts.PainterOptions{ 44 | OutputFormat: charts.ChartOutputPNG, 45 | Width: 1000, 46 | Height: 400, 47 | }) 48 | defaultPainter := p.Child(charts.PainterBoxOption(charts.NewBox(0, 0, 500, 400))) 49 | opt.Title.Text = "Bar Chart Top Label" 50 | if err := defaultPainter.BarChart(opt); err != nil { 51 | panic(err) 52 | } 53 | bottomLabelPainter := p.Child(charts.PainterBoxOption(charts.NewBox(500, 0, 1000, 400))) 54 | opt.Title.Text = "Bar Chart Bottom Label" 55 | opt.SeriesLabelPosition = charts.PositionBottom 56 | if err := bottomLabelPainter.BarChart(opt); err != nil { 57 | panic(err) 58 | } 59 | if buf, err := p.Bytes(); err != nil { 60 | panic(err) 61 | } else if err = writeFile(buf); err != nil { 62 | panic(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/1-Painter/bar_chart-4-mark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart with mark points and mark lines shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "bar-chart-4-mark.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3}, 27 | {2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3}, 28 | } 29 | 30 | opt := charts.NewBarChartOptionWithData(values) 31 | opt.XAxis.Labels = []string{ 32 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 33 | } 34 | opt.Legend = charts.LegendOption{ 35 | SeriesNames: []string{ 36 | "Rainfall", "Evaporation", 37 | }, 38 | Offset: charts.OffsetRight, 39 | OverlayChart: charts.Ptr(true), 40 | } 41 | valueFormat := func(v float64) string { 42 | return charts.FormatValueHumanizeShort(v, 0, false) 43 | } 44 | opt.SeriesList[0].MarkLine.ValueFormatter = valueFormat 45 | opt.SeriesList[0].MarkLine.AddLines(charts.SeriesMarkTypeAverage) 46 | opt.SeriesList[0].MarkPoint.ValueFormatter = valueFormat 47 | opt.SeriesList[0].MarkPoint.AddPoints( 48 | charts.SeriesMarkTypeMax, 49 | charts.SeriesMarkTypeMin, 50 | ) 51 | opt.SeriesList[1].MarkLine.ValueFormatter = valueFormat 52 | opt.SeriesList[1].MarkLine.AddLines(charts.SeriesMarkTypeAverage) 53 | opt.SeriesList[1].MarkPoint.ValueFormatter = valueFormat 54 | opt.SeriesList[1].MarkPoint.AddPoints( 55 | charts.SeriesMarkTypeMax, 56 | charts.SeriesMarkTypeMin, 57 | ) 58 | 59 | p := charts.NewPainter(charts.PainterOptions{ 60 | OutputFormat: charts.ChartOutputPNG, 61 | Width: 600, 62 | Height: 400, 63 | }) 64 | if err := p.BarChart(opt); err != nil { 65 | panic(err) 66 | } else if buf, err := p.Bytes(); err != nil { 67 | panic(err) 68 | } else if err = writeFile(buf); err != nil { 69 | panic(err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/1-Painter/doughnut_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example doughnut chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "doughnut-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{ 26 | 1048, 735, 580, 484, 300, 27 | } 28 | 29 | opt := charts.NewDoughnutChartOptionWithData(values) 30 | opt.Title = charts.TitleOption{ 31 | Text: "Doughnut Chart", 32 | Subtext: "(Fake Data)", 33 | Offset: charts.OffsetCenter, 34 | FontStyle: charts.NewFontStyleWithSize(16), 35 | SubtextFontStyle: charts.NewFontStyleWithSize(10), 36 | } 37 | opt.Padding = charts.NewBoxEqual(20) 38 | opt.Legend = charts.LegendOption{ 39 | SeriesNames: []string{ 40 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 41 | }, 42 | Vertical: charts.Ptr(true), 43 | Offset: charts.OffsetStr{ 44 | Left: "80%", 45 | Top: charts.PositionBottom, 46 | }, 47 | FontStyle: charts.NewFontStyleWithSize(10), 48 | } 49 | 50 | p := charts.NewPainter(charts.PainterOptions{ 51 | OutputFormat: charts.ChartOutputPNG, 52 | Width: 600, 53 | Height: 400, 54 | }) 55 | if err := p.DoughnutChart(opt); err != nil { 56 | panic(err) 57 | } else if buf, err := p.Bytes(); err != nil { 58 | panic(err) 59 | } else if err = writeFile(buf); err != nil { 60 | panic(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/1-Painter/funnel_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example funnel chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "funnel-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{100, 80, 60, 40, 20, 10, 2} 26 | 27 | opt := charts.NewFunnelChartOptionWithData(values) 28 | opt.Title.Text = "Funnel" 29 | opt.Legend.SeriesNames = []string{ 30 | "Show", "Click", "Visit", "Inquiry", "Order", "Pay", "Cancel", 31 | } 32 | opt.Legend.Padding = charts.Box{Left: 100} 33 | 34 | p := charts.NewPainter(charts.PainterOptions{ 35 | OutputFormat: charts.ChartOutputPNG, 36 | Width: 600, 37 | Height: 400, 38 | }) 39 | if err := p.FunnelChart(opt); err != nil { 40 | panic(err) 41 | } else if buf, err := p.Bytes(); err != nil { 42 | panic(err) 43 | } else if err = writeFile(buf); err != nil { 44 | panic(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/1-Painter/heat_map-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example heat map chart using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "heat-map-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {4.4, 4.9, 7.0, 7.5, 4.3}, 27 | {2.6, 5.9, 9.0, 6.4, 2.3}, 28 | {3.3, 6.4, 7.0, 4.9, 3.2}, 29 | {1.9, 6.0, 9.0, 5.9, 2.6}, 30 | {4.4, 5.9, 7.0, 6.4, 4.6}, 31 | } 32 | 33 | opt := charts.NewHeatMapOptionWithData(values) 34 | opt.Title.Text = "Heat Map Chart" 35 | opt.Title.Offset = charts.OffsetCenter 36 | opt.XAxis.Title = "X-Axis" 37 | opt.YAxis.Title = "Y-Axis" 38 | 39 | p := charts.NewPainter(charts.PainterOptions{ 40 | OutputFormat: charts.ChartOutputPNG, 41 | Width: 600, 42 | Height: 400, 43 | }) 44 | if err := p.HeatMapChart(opt); err != nil { 45 | panic(err) 46 | } else if buf, err := p.Bytes(); err != nil { 47 | panic(err) 48 | } else if err = writeFile(buf); err != nil { 49 | panic(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/1-Painter/horizontal_bar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example horizontal bar chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "horizontal-bar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {10, 30, 50, 70, 90, 110, 130}, 27 | {20, 40, 60, 80, 100, 120, 140}, 28 | } 29 | 30 | opt := charts.NewHorizontalBarChartOptionWithData(values) 31 | opt.Title.Text = "World Population" 32 | opt.Padding = charts.Box{ 33 | Top: 20, 34 | Right: 40, 35 | Bottom: 20, 36 | Left: 20, 37 | } 38 | opt.Legend.SeriesNames = []string{ 39 | "2011", "2012", 40 | } 41 | opt.YAxis = charts.YAxisOption{ 42 | Labels: []string{ 43 | "UN", "Brazil", "Indonesia", "USA", "India", "China", "World", 44 | }, 45 | } 46 | 47 | p := charts.NewPainter(charts.PainterOptions{ 48 | OutputFormat: charts.ChartOutputPNG, 49 | Width: 600, 50 | Height: 400, 51 | }) 52 | if err := p.HorizontalBarChart(opt); err != nil { 53 | panic(err) 54 | } else if buf, err := p.Bytes(); err != nil { 55 | panic(err) 56 | } else if err = writeFile(buf); err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/1-Painter/horizontal_bar_chart-2-size_margin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example horizontal bar chart with custom bar sizes and margins. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "horizontal_bar-chart-2-size_margin.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {2.0, 4.9, 7.0, 23.2, 25.6, 76.7}, 27 | {2.6, 5.9, 9.0, 26.4, 28.7, 70.7}, 28 | } 29 | 30 | opt := charts.NewHorizontalBarChartOptionWithData(values) 31 | opt.XAxis.Labels = []string{ 32 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", 33 | } 34 | opt.Legend.Show = charts.Ptr(false) 35 | 36 | p := charts.NewPainter(charts.PainterOptions{ 37 | OutputFormat: charts.ChartOutputPNG, 38 | Width: 1200, 39 | Height: 400, 40 | }) 41 | defaultPainter := p.Child(charts.PainterBoxOption(charts.NewBox(0, 0, 400, 400))) 42 | opt.Title.Text = "Default" 43 | if err := defaultPainter.HorizontalBarChart(opt); err != nil { 44 | panic(err) 45 | } 46 | barSizePainter := p.Child(charts.PainterBoxOption(charts.NewBox(400, 0, 800, 400))) 47 | opt.Title.Text = "Small Bar" 48 | opt.BarHeight = 4 49 | if err := barSizePainter.HorizontalBarChart(opt); err != nil { 50 | panic(err) 51 | } 52 | marginPainter := p.Child(charts.PainterBoxOption(charts.NewBox(800, 0, 1200, 400))) 53 | opt.Title.Text = "No Margin" 54 | opt.BarMargin = charts.Ptr(0.0) 55 | opt.BarHeight = 0 // reset to default size 56 | if err := marginPainter.HorizontalBarChart(opt); err != nil { 57 | panic(err) 58 | } 59 | 60 | if buf, err := p.Bytes(); err != nil { 61 | panic(err) 62 | } else if err = writeFile(buf); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/1-Painter/horizontal_bar_chart-3-mark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart with mark points and mark lines shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "horizontal-bar-chart-3-mark.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {10, 30, 50, 70, 90, 110, 130}, 27 | {20, 40, 60, 80, 100, 120, 140}, 28 | } 29 | 30 | opt := charts.NewHorizontalBarChartOptionWithData(values) 31 | opt.Title.Text = "World Population" 32 | opt.Padding = charts.Box{ 33 | Top: 20, 34 | Right: 40, 35 | Bottom: 20, 36 | Left: 20, 37 | } 38 | opt.Legend.SeriesNames = []string{ 39 | "2011", "2012", 40 | } 41 | opt.YAxis = charts.YAxisOption{ 42 | Labels: []string{ 43 | "UN", "Brazil", "Indonesia", "USA", "India", "China", "World", 44 | }, 45 | } 46 | opt.SeriesList[0].MarkLine.AddLines(charts.SeriesMarkTypeMax) 47 | opt.SeriesList[1].MarkLine.AddLines(charts.SeriesMarkTypeMax) 48 | 49 | p := charts.NewPainter(charts.PainterOptions{ 50 | OutputFormat: charts.ChartOutputPNG, 51 | Width: 600, 52 | Height: 400, 53 | }) 54 | if err := p.HorizontalBarChart(opt); err != nil { 55 | panic(err) 56 | } else if buf, err := p.Bytes(); err != nil { 57 | panic(err) 58 | } else if err = writeFile(buf); err != nil { 59 | panic(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/1-Painter/horizontal_bar_chart-4-stacked/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example of a "Stacked" horizontal bar chart. Stacked charts are a good way to represent data where the sum is important, 12 | and you want to show what components produce that sum. 13 | */ 14 | 15 | func writeFile(buf []byte) error { 16 | tmpPath := "./tmp" 17 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 18 | return err 19 | } 20 | 21 | file := filepath.Join(tmpPath, "horizontal-bar-chart-4-stacked.png") 22 | return os.WriteFile(file, buf, 0600) 23 | } 24 | 25 | func main() { 26 | values := [][]float64{ 27 | {10, 30, 50, 70, 90, 110, 130}, 28 | {20, 40, 60, 80, 100, 120, 140}, 29 | } 30 | 31 | opt := charts.NewHorizontalBarChartOptionWithData(values) 32 | opt.Title.Text = "Some Numbers" 33 | opt.Padding = charts.Box{ 34 | Top: 20, 35 | Right: 20, 36 | Bottom: 0, 37 | Left: 20, 38 | } 39 | opt.StackSeries = charts.Ptr(true) 40 | for i := range opt.SeriesList { 41 | opt.SeriesList[i].Label.Show = charts.Ptr(true) 42 | } 43 | opt.Legend.SeriesNames = []string{ 44 | "2011", "2012", 45 | } 46 | opt.XAxis.Show = charts.Ptr(false) 47 | opt.YAxis = charts.YAxisOption{ 48 | Labels: []string{ 49 | "UN", "Brazil", "Indonesia", "USA", "India", "China", "World", 50 | }, 51 | } 52 | 53 | p := charts.NewPainter(charts.PainterOptions{ 54 | OutputFormat: charts.ChartOutputPNG, 55 | Width: 600, 56 | Height: 400, 57 | }) 58 | if err := p.HorizontalBarChart(opt); err != nil { 59 | panic(err) 60 | } else if buf, err := p.Bytes(); err != nil { 61 | panic(err) 62 | } else if err = writeFile(buf); err != nil { 63 | panic(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, charts.GetNullValue(), 90, 230, 210}, 27 | {220, 182, 191, 234, 290, 330, 310}, 28 | {150, 232, 201, 154, 190, 330, 410}, 29 | {320, 332, 301, 334, 390, 330, 320}, 30 | {820, 932, 901, 934, 1290, 1330, 1320}, 31 | } 32 | 33 | opt := charts.NewLineChartOptionWithData(values) 34 | opt.Title.Text = "Line" 35 | opt.Title.FontStyle.FontSize = 16 36 | opt.XAxis.Labels = []string{ 37 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 38 | } 39 | opt.Legend.SeriesNames = []string{ 40 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 41 | } 42 | opt.Legend.Padding = charts.Box{ 43 | Left: 100, 44 | } 45 | opt.Symbol = charts.SymbolCircle 46 | opt.LineStrokeWidth = 1.2 47 | 48 | p := charts.NewPainter(charts.PainterOptions{ 49 | OutputFormat: charts.ChartOutputPNG, 50 | Width: 600, 51 | Height: 400, 52 | }) 53 | if err := p.LineChart(opt); err != nil { 54 | panic(err) 55 | } else if buf, err := p.Bytes(); err != nil { 56 | panic(err) 57 | } else if err = writeFile(buf); err != nil { 58 | panic(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-2-symbols/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart which sets a different symbol for each series item. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-2-symbols.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 96, 90, 230, 210}, 27 | {220, 182, 191, 234, 290, 330, 310}, 28 | {150, 232, 201, 154, 190, 330, 410}, 29 | {320, 332, 301, 334, 390, 330, 320}, 30 | } 31 | 32 | opt := charts.NewLineChartOptionWithData(values) 33 | opt.XAxis.Labels = []string{ 34 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 35 | } 36 | opt.Legend.SeriesNames = []string{ 37 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 38 | } 39 | opt.LineStrokeWidth = 1.2 40 | opt.SeriesList[0].Symbol = charts.SymbolCircle 41 | opt.SeriesList[1].Symbol = charts.SymbolDiamond 42 | opt.SeriesList[2].Symbol = charts.SymbolSquare 43 | opt.SeriesList[3].Symbol = charts.SymbolDot 44 | 45 | p := charts.NewPainter(charts.PainterOptions{ 46 | OutputFormat: charts.ChartOutputPNG, 47 | Width: 600, 48 | Height: 400, 49 | }) 50 | if err := p.LineChart(opt); err != nil { 51 | panic(err) 52 | } else if buf, err := p.Bytes(); err != nil { 53 | panic(err) 54 | } else if err = writeFile(buf); err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-3-smooth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart with bold smooth lines. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-3-smooth.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 96, 90, 230, 210}, 27 | {220, 182, 191, 234, 290, 330, 310}, 28 | {150, 232, 201, 154, 190, 330, 410}, 29 | {320, 332, 301, 334, 390, 330, 320}, 30 | } 31 | 32 | opt := charts.NewLineChartOptionWithData(values) 33 | opt.XAxis.Labels = []string{ 34 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 35 | } 36 | opt.Legend.SeriesNames = []string{ 37 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 38 | } 39 | opt.Legend.Show = charts.Ptr(false) 40 | opt.Symbol = charts.SymbolNone 41 | opt.LineStrokeWidth = 4.0 42 | opt.StrokeSmoothingTension = 0.9 43 | 44 | p := charts.NewPainter(charts.PainterOptions{ 45 | OutputFormat: charts.ChartOutputPNG, 46 | Width: 600, 47 | Height: 400, 48 | }) 49 | if err := p.LineChart(opt); err != nil { 50 | panic(err) 51 | } else if buf, err := p.Bytes(); err != nil { 52 | panic(err) 53 | } else if err = writeFile(buf); err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-4-mark/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart with mark points and mark lines configured. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-4-mark.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 95, 90, 230, 210}, 27 | {320, 332, 301, 334, 390, 330, 320}, 28 | {820, 932, 901, 934, 1290, 1330, 1320}, 29 | } 30 | 31 | opt := charts.NewLineChartOptionWithData(values) 32 | opt.Padding = charts.NewBox(20, 20, 48, 20) 33 | opt.Title.FontStyle.FontSize = 16 34 | opt.XAxis.Labels = []string{ 35 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 36 | } 37 | opt.Legend.SeriesNames = []string{ 38 | "Email", "Direct", "Search Engine", 39 | } 40 | opt.Symbol = charts.SymbolCircle 41 | opt.LineStrokeWidth = 1.2 42 | for i := range opt.SeriesList { 43 | opt.SeriesList[i].MarkPoint.AddPoints(charts.SeriesMarkTypeMax) 44 | opt.SeriesList[i].MarkLine.AddLines(charts.SeriesMarkTypeAverage) 45 | opt.SeriesList[i].MarkLine.ValueFormatter = func(v float64) string { 46 | return charts.FormatValueHumanizeShort(v, 1, false) 47 | } 48 | } 49 | 50 | p := charts.NewPainter(charts.PainterOptions{ 51 | OutputFormat: charts.ChartOutputPNG, 52 | Width: 600, 53 | Height: 400, 54 | }) 55 | if err := p.LineChart(opt); err != nil { 56 | panic(err) 57 | } else if buf, err := p.Bytes(); err != nil { 58 | panic(err) 59 | } else if err = writeFile(buf); err != nil { 60 | panic(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-5-area/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example line chart with the area below the line shaded. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | file := filepath.Join(tmpPath, "line-chart-5-area.png") 20 | return os.WriteFile(file, buf, 0600) 21 | } 22 | 23 | func main() { 24 | values := [][]float64{ 25 | {120, 132, 101, 134, 90, 230, 210}, 26 | } 27 | 28 | opt := charts.NewLineChartOptionWithData(values) 29 | opt.Title.Text = "Line" 30 | opt.XAxis.Labels = []string{ 31 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 32 | } 33 | opt.Legend.SeriesNames = []string{"Email"} 34 | opt.Legend.Padding = charts.Box{ 35 | Top: 5, 36 | Bottom: 10, 37 | } 38 | opt.YAxis[0].Min = charts.Ptr(0.0) // Ensure y-axis starts at 0 39 | 40 | // Setup fill styling below 41 | opt.FillArea = charts.Ptr(true) // Enable fill area 42 | opt.FillOpacity = 150 // Set fill opacity 43 | opt.XAxis.BoundaryGap = charts.Ptr(false) // Disable boundary gap 44 | 45 | p := charts.NewPainter(charts.PainterOptions{ 46 | OutputFormat: charts.ChartOutputPNG, 47 | Width: 600, 48 | Height: 400, 49 | }) 50 | if err := p.LineChart(opt); err != nil { 51 | panic(err) 52 | } else if buf, err := p.Bytes(); err != nil { 53 | panic(err) 54 | } else if err := writeFile(buf); err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-7-boundary_gap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart demonstrating the spacing between boundary gaps. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-7-boundary_gap.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 90, 230, 210}, 27 | {220, 182, 191, 290, 330, 310}, 28 | {150, 232, 201, 190, 330, 410}, 29 | {320, 332, 301, 390, 330, 320}, 30 | {820, 932, 901, 1290, 1330, 1320}, 31 | } 32 | 33 | opt := charts.NewLineChartOptionWithData(values) 34 | opt.Padding = charts.NewBoxEqual(10) 35 | opt.Title.FontStyle.FontSize = 16 36 | opt.XAxis.Labels = []string{ 37 | "A", "B", "C", "D", "E", "F", 38 | } 39 | opt.Legend.Show = charts.Ptr(false) 40 | opt.Symbol = charts.SymbolCircle 41 | 42 | p := charts.NewPainter(charts.PainterOptions{ 43 | OutputFormat: charts.ChartOutputPNG, 44 | Width: 1200, 45 | Height: 400, 46 | }) 47 | boundaryGapPainter := p.Child(charts.PainterBoxOption(charts.NewBox(0, 0, 600, 400))) 48 | opt.Title.Text = "Boundary Gap" 49 | if err := boundaryGapPainter.LineChart(opt); err != nil { 50 | panic(err) 51 | } 52 | boundaryGapDisabledPainter := p.Child(charts.PainterBoxOption(charts.NewBox(600, 0, 1200, 400))) 53 | opt.XAxis.BoundaryGap = charts.Ptr(false) 54 | opt.Title.Text = "Boundary Gap Disabled" 55 | if err := boundaryGapDisabledPainter.LineChart(opt); err != nil { 56 | panic(err) 57 | } 58 | if buf, err := p.Bytes(); err != nil { 59 | panic(err) 60 | } else if err = writeFile(buf); err != nil { 61 | panic(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/1-Painter/line_chart-8-dual_y_axis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic line chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-8-dual_y_axis.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 134, 90, 230, 210}, 27 | {820, 932, 901, 934, 1290, 1330, 1320}, 28 | } 29 | opt := charts.NewLineChartOptionWithData(values) 30 | opt.Title.Text = "Dual Axis Line" 31 | opt.XAxis = charts.XAxisOption{ 32 | Labels: []string{"A", "B", "C", "D", "E", "F", "G"}, 33 | } 34 | opt.Legend = charts.LegendOption{ 35 | SeriesNames: []string{"Left Series", "Right Series"}, 36 | } 37 | opt.SeriesList[1].YAxisIndex = 1 38 | opt.YAxis = append(opt.YAxis, opt.YAxis[0]) 39 | opt.YAxis[0].Theme = opt.Theme.WithYAxisSeriesColor(0) 40 | opt.YAxis[1].Theme = opt.Theme.WithYAxisSeriesColor(1) 41 | 42 | p := charts.NewPainter(charts.PainterOptions{ 43 | OutputFormat: charts.ChartOutputPNG, 44 | Width: 600, 45 | Height: 400, 46 | }) 47 | if err := p.LineChart(opt); err != nil { 48 | panic(err) 49 | } else if buf, err := p.Bytes(); err != nil { 50 | panic(err) 51 | } else if err = writeFile(buf); err != nil { 52 | panic(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/1-Painter/pie_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example pie chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "pie-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{ 26 | 1048, 735, 580, 484, 300, 27 | } 28 | 29 | opt := charts.NewPieChartOptionWithData(values) 30 | opt.Title = charts.TitleOption{ 31 | Text: "Pie Chart", 32 | Subtext: "(Fake Data)", 33 | Offset: charts.OffsetCenter, 34 | FontStyle: charts.NewFontStyleWithSize(16), 35 | SubtextFontStyle: charts.NewFontStyleWithSize(10), 36 | } 37 | opt.Padding = charts.NewBoxEqual(20) 38 | opt.Legend = charts.LegendOption{ 39 | SeriesNames: []string{ 40 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 41 | }, 42 | Vertical: charts.Ptr(true), 43 | Offset: charts.OffsetStr{ 44 | Left: "80%", 45 | Top: charts.PositionBottom, 46 | }, 47 | FontStyle: charts.NewFontStyleWithSize(10), 48 | } 49 | 50 | p := charts.NewPainter(charts.PainterOptions{ 51 | OutputFormat: charts.ChartOutputPNG, 52 | Width: 600, 53 | Height: 400, 54 | }) 55 | if err := p.PieChart(opt); err != nil { 56 | panic(err) 57 | } else if buf, err := p.Bytes(); err != nil { 58 | panic(err) 59 | } else if err = writeFile(buf); err != nil { 60 | panic(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/1-Painter/pie_chart-2-series_radius/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | 9 | "github.com/go-analyze/charts" 10 | ) 11 | 12 | /* 13 | Example pie chart which varies the series radius by the percentage of the series. 14 | */ 15 | 16 | func writeFile(buf []byte) error { 17 | tmpPath := "./tmp" 18 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 19 | return err 20 | } 21 | 22 | file := filepath.Join(tmpPath, "pie-chart-2-series_radius.png") 23 | return os.WriteFile(file, buf, 0600) 24 | } 25 | 26 | func main() { 27 | values := []float64{ 28 | 1048, 735, 580, 484, 300, 29 | } 30 | 31 | opt := charts.NewPieChartOptionWithData(values) 32 | opt.Title = charts.TitleOption{ 33 | Text: "Pie Chart", 34 | Subtext: "(Fake Data)", 35 | Offset: charts.OffsetCenter, 36 | FontStyle: charts.NewFontStyleWithSize(16), 37 | SubtextFontStyle: charts.NewFontStyleWithSize(10), 38 | } 39 | opt.Padding = charts.NewBoxEqual(20) 40 | opt.Legend = charts.LegendOption{ 41 | SeriesNames: []string{ 42 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 43 | }, 44 | Vertical: charts.Ptr(true), 45 | Offset: charts.OffsetStr{ 46 | Left: "80%", 47 | Top: charts.PositionBottom, 48 | }, 49 | FontStyle: charts.NewFontStyleWithSize(10), 50 | } 51 | largestRadius := 120.0 52 | seriesMax := opt.SeriesList.MaxValue() 53 | for i := range opt.SeriesList { 54 | radiusValue := largestRadius * math.Sqrt(opt.SeriesList[i].Value/seriesMax) 55 | opt.SeriesList[i].Radius = strconv.Itoa(int(math.Ceil(radiusValue))) 56 | } 57 | 58 | p := charts.NewPainter(charts.PainterOptions{ 59 | OutputFormat: charts.ChartOutputPNG, 60 | Width: 600, 61 | Height: 400, 62 | }) 63 | if err := p.PieChart(opt); err != nil { 64 | panic(err) 65 | } else if buf, err := p.Bytes(); err != nil { 66 | panic(err) 67 | } else if err = writeFile(buf); err != nil { 68 | panic(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/1-Painter/pie_chart-3-gap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example pie chart with a segment gap configured. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "pie-chart-3-gap.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{ 26 | 1048, 735, 580, 484, 300, 27 | } 28 | 29 | opt := charts.NewPieChartOptionWithData(values) 30 | opt.Title = charts.TitleOption{ 31 | Text: "Pie Chart With Segment Gap", 32 | Offset: charts.OffsetCenter, 33 | FontStyle: charts.NewFontStyleWithSize(16), 34 | } 35 | opt.Legend = charts.LegendOption{ 36 | Show: charts.Ptr(false), 37 | SeriesNames: []string{ 38 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 39 | }, 40 | } 41 | opt.SegmentGap = 16 42 | 43 | p := charts.NewPainter(charts.PainterOptions{ 44 | OutputFormat: charts.ChartOutputPNG, 45 | Width: 600, 46 | Height: 400, 47 | }) 48 | if err := p.PieChart(opt); err != nil { 49 | panic(err) 50 | } else if buf, err := p.Bytes(); err != nil { 51 | panic(err) 52 | } else if err = writeFile(buf); err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/1-Painter/radar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example radar chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "radar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {4200, 3000, 20000, 35000, 50000, 18000}, 27 | {5000, 14000, 28000, 26000, 42000, 21000}, 28 | } 29 | 30 | opt := charts.NewRadarChartOptionWithData(values, 31 | []string{ 32 | "Sales", 33 | "Administration", 34 | "Information Technology", 35 | "Customer Support", 36 | "Development", 37 | "Marketing", 38 | }, 39 | []float64{ 40 | 6500, 41 | 16000, 42 | 30000, 43 | 38000, 44 | 52000, 45 | 25000, 46 | }) 47 | opt.Title = charts.TitleOption{ 48 | Text: "Basic Radar Chart", 49 | FontStyle: charts.NewFontStyleWithSize(16), 50 | } 51 | opt.Legend = charts.LegendOption{ 52 | SeriesNames: []string{ 53 | "Allocated Budget", "Actual Spending", 54 | }, 55 | Offset: charts.OffsetRight, 56 | } 57 | 58 | p := charts.NewPainter(charts.PainterOptions{ 59 | OutputFormat: charts.ChartOutputPNG, 60 | Width: 600, 61 | Height: 400, 62 | }) 63 | if err := p.RadarChart(opt); err != nil { 64 | panic(err) 65 | } else if buf, err := p.Bytes(); err != nil { 66 | panic(err) 67 | } else if err = writeFile(buf); err != nil { 68 | panic(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/1-Painter/scatter_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic scatter chart with a variety of basic configuration options shown using the Painter API. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "scatter-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, charts.GetNullValue(), 90, 230, 210}, 27 | {220, 182, 191, 234, 290, 330, 310}, 28 | {150, 232, 201, 154, 190, 330, 410}, 29 | {320, 332, 301, 334, 390, 330, 320}, 30 | {820, 932, 901, 934, 1290, 1330, 1320}, 31 | } 32 | 33 | opt := charts.NewScatterChartOptionWithData(values) 34 | opt.Title.Text = "Scatter" 35 | opt.Title.FontStyle.FontSize = 16 36 | opt.XAxis.Labels = []string{ 37 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 38 | } 39 | opt.Legend.SeriesNames = []string{ 40 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 41 | } 42 | opt.Legend.Padding = charts.Box{ 43 | Left: 100, 44 | } 45 | opt.Symbol = charts.SymbolDot 46 | opt.SymbolSize = 4 47 | 48 | p := charts.NewPainter(charts.PainterOptions{ 49 | OutputFormat: charts.ChartOutputPNG, 50 | Width: 600, 51 | Height: 400, 52 | }) 53 | if err := p.ScatterChart(opt); err != nil { 54 | panic(err) 55 | } else if buf, err := p.Bytes(); err != nil { 56 | panic(err) 57 | } else if err = writeFile(buf); err != nil { 58 | panic(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/1-Painter/scatter_chart-2-symbols/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example basic scatter chart showing per-series symbols. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "scatter-chart-2-symbols.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 95, 90, 230, 210}, 27 | {220, 182, 191, 234, 290, 330, 310}, 28 | {150, 232, 201, 154, 190, 330, 410}, 29 | {320, 332, 301, 334, 390, 330, 320}, 30 | } 31 | 32 | opt := charts.NewScatterChartOptionWithData(values) 33 | opt.Title.FontStyle.FontSize = 16 34 | opt.XAxis.Labels = []string{ 35 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 36 | } 37 | opt.Legend.SeriesNames = []string{ 38 | "Email", "Union Ads", "Video Ads", "Direct", 39 | } 40 | opt.SymbolSize = 4 41 | opt.SeriesList[0].Symbol = charts.SymbolCircle 42 | opt.SeriesList[1].Symbol = charts.SymbolDiamond 43 | opt.SeriesList[2].Symbol = charts.SymbolSquare 44 | opt.SeriesList[3].Symbol = charts.SymbolDot 45 | 46 | p := charts.NewPainter(charts.PainterOptions{ 47 | OutputFormat: charts.ChartOutputPNG, 48 | Width: 600, 49 | Height: 400, 50 | }) 51 | if err := p.ScatterChart(opt); err != nil { 52 | panic(err) 53 | } else if buf, err := p.Bytes(); err != nil { 54 | panic(err) 55 | } else if err = writeFile(buf); err != nil { 56 | panic(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/bar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example bar chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "bar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20.0, 6.4, 3.3}, 27 | {2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3}, 28 | } 29 | p, err := charts.BarRender( 30 | values, 31 | charts.XAxisLabelsOptionFunc([]string{ 32 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 33 | }), 34 | charts.LegendOptionFunc(charts.LegendOption{ 35 | SeriesNames: []string{ 36 | "Rainfall", "Evaporation", 37 | }, 38 | Offset: charts.OffsetRight, 39 | OverlayChart: charts.Ptr(true), 40 | }), 41 | charts.MarkLineOptionFunc(0, charts.SeriesMarkTypeAverage), 42 | charts.MarkPointOptionFunc(0, charts.SeriesMarkTypeMax, 43 | charts.SeriesMarkTypeMin), 44 | func(opt *charts.ChartOption) { 45 | opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( 46 | charts.SeriesMarkTypeMax, 47 | charts.SeriesMarkTypeMin, 48 | ) 49 | opt.SeriesList[1].MarkLine = charts.NewMarkLine( 50 | charts.SeriesMarkTypeAverage, 51 | ) 52 | }, 53 | ) 54 | if err != nil { 55 | panic(err) 56 | } else if buf, err := p.Bytes(); err != nil { 57 | panic(err) 58 | } else if err = writeFile(buf); err != nil { 59 | panic(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/chinese/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example showing how to load in custom fonts. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "chinese-line-chart.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | // Download font files from: https://github.com/googlefonts/noto-cjk 26 | if buf, err := os.ReadFile("./NotoSansSC.ttf"); err != nil { 27 | panic(err) 28 | } else if err = charts.InstallFont("noto", buf); err != nil { 29 | panic(err) 30 | } 31 | // in this example we just change the global default font 32 | charts.SetDefaultFont("noto") 33 | // It's also possible to specify the font on the chart configuration (for example on the title, or legend specifically) 34 | 35 | values := [][]float64{ 36 | {120, 132, 101, 134, 90, 230, 210}, 37 | {220, 182, 191, 234, 290, 330, 310}, 38 | {150, 232, 201, 154, 190, 330, 410}, 39 | {320, 332, 301, 334, 390, 330, 320}, 40 | {820, 932, 901, 934, 1290, 1330, 1320}, 41 | } 42 | p, err := charts.LineRender( 43 | values, 44 | charts.TitleTextOptionFunc("测试"), 45 | charts.XAxisLabelsOptionFunc([]string{ 46 | "星期一", 47 | "星期二", 48 | "星期三", 49 | "星期四", 50 | "星期五", 51 | "星期六", 52 | "星期日", 53 | }), 54 | charts.LegendLabelsOptionFunc([]string{ 55 | "邮件", 56 | "广告", 57 | "视频广告", 58 | "直接访问", 59 | "搜索引擎", 60 | }), 61 | ) 62 | if err != nil { 63 | panic(err) 64 | } else if buf, err := p.Bytes(); err != nil { 65 | panic(err) 66 | } else if err = writeFile(buf); err != nil { 67 | panic(err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/doughnut_chart-1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example doughnut chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "doughnut-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{ 26 | 1048, 735, 580, 484, 300, 27 | } 28 | p, err := charts.DoughnutRender( 29 | values, 30 | charts.TitleOptionFunc(charts.TitleOption{ 31 | Text: "Doughnut Chart", 32 | Subtext: "(Fake Data)", 33 | Offset: charts.OffsetCenter, 34 | FontStyle: charts.NewFontStyleWithSize(16), 35 | SubtextFontStyle: charts.NewFontStyleWithSize(10), 36 | }), 37 | charts.PaddingOptionFunc(charts.NewBoxEqual(20)), 38 | charts.LegendOptionFunc(charts.LegendOption{ 39 | SeriesNames: []string{ 40 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 41 | }, 42 | Vertical: charts.Ptr(true), 43 | Offset: charts.OffsetStr{ 44 | Left: "80%", 45 | Top: charts.PositionBottom, 46 | }, 47 | FontStyle: charts.NewFontStyleWithSize(10), 48 | }), 49 | ) 50 | if err != nil { 51 | panic(err) 52 | } else if buf, err := p.Bytes(); err != nil { 53 | panic(err) 54 | } else if err = writeFile(buf); err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/funnel_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example funnel chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "funnel-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{100, 80, 60, 40, 20, 10, 2} 26 | 27 | p, err := charts.FunnelRender( 28 | values, 29 | charts.TitleTextOptionFunc("Funnel"), 30 | charts.LegendLabelsOptionFunc([]string{ 31 | "Show", "Click", "Visit", "Inquiry", "Order", "Pay", "Cancel", 32 | }), 33 | func(opt *charts.ChartOption) { 34 | opt.Legend.Padding = charts.Box{Left: 100} 35 | }, 36 | ) 37 | if err != nil { 38 | panic(err) 39 | } else if buf, err := p.Bytes(); err != nil { 40 | panic(err) 41 | } else if err = writeFile(buf); err != nil { 42 | panic(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/horizontal_bar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example horizontal bar chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "horizontal-bar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {10, 30, 50, 70, 90, 110, 130}, 27 | {20, 40, 60, 80, 100, 120, 140}, 28 | } 29 | p, err := charts.HorizontalBarRender( 30 | values, 31 | charts.TitleTextOptionFunc("World Population"), 32 | charts.PaddingOptionFunc(charts.Box{ 33 | Top: 20, 34 | Right: 40, 35 | Bottom: 20, 36 | Left: 20, 37 | }), 38 | charts.LegendLabelsOptionFunc([]string{ 39 | "2011", "2012", 40 | }), 41 | charts.YAxisLabelsOptionFunc([]string{ 42 | "UN", "Brazil", "Indonesia", "USA", "India", "China", "World", 43 | }), 44 | ) 45 | if err != nil { 46 | panic(err) 47 | } else if buf, err := p.Bytes(); err != nil { 48 | panic(err) 49 | } else if err = writeFile(buf); err != nil { 50 | panic(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/line_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-analyze/charts" 9 | ) 10 | 11 | /* 12 | Example basic line chart with a variety of basic configuration options shown. 13 | */ 14 | 15 | func writeFile(buf []byte) error { 16 | tmpPath := "./tmp" 17 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 18 | return err 19 | } 20 | 21 | file := filepath.Join(tmpPath, "line-chart-1-basic.png") 22 | return os.WriteFile(file, buf, 0600) 23 | } 24 | 25 | func main() { 26 | values := [][]float64{ 27 | {120, 132, 101, charts.GetNullValue(), 90, 230, 210}, 28 | {220, 182, 191, 234, 290, 330, 310}, 29 | {150, 232, 201, 154, 190, 330, 410}, 30 | {320, 332, 301, 334, 390, 330, 320}, 31 | {820, 932, 901, 934, 1290, 1330, 1320}, 32 | } 33 | p, err := charts.LineRender( 34 | values, 35 | charts.TitleTextOptionFunc("Line"), 36 | charts.XAxisLabelsOptionFunc([]string{ 37 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 38 | }), 39 | charts.LegendLabelsOptionFunc([]string{ 40 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 41 | }), 42 | func(opt *charts.ChartOption) { 43 | opt.Title.FontStyle.FontSize = 16 44 | opt.Legend.Padding = charts.Box{ 45 | Left: 100, 46 | } 47 | opt.Symbol = charts.SymbolCircle 48 | opt.LineStrokeWidth = 1.2 49 | opt.ValueFormatter = func(f float64) string { 50 | return fmt.Sprintf("%.0f", f) 51 | } 52 | }, 53 | ) 54 | if err != nil { 55 | panic(err) 56 | } else if buf, err := p.Bytes(); err != nil { 57 | panic(err) 58 | } else if err = writeFile(buf); err != nil { 59 | panic(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/line_chart-3-area/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example line chart with the area below the line shaded. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "line-chart-3-area.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 134, 90, 230, 210}, 27 | } 28 | p, err := charts.LineRender( 29 | values, 30 | charts.TitleTextOptionFunc("Line"), 31 | charts.XAxisLabelsOptionFunc([]string{ 32 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 33 | }), 34 | charts.LegendOptionFunc(charts.LegendOption{ 35 | SeriesNames: []string{"Email"}, 36 | Padding: charts.Box{ 37 | Top: 5, 38 | Bottom: 10, 39 | }, 40 | }), 41 | charts.YAxisOptionFunc(charts.YAxisOption{ 42 | Min: charts.Ptr(0.0), // ensure y-axis starts at 0 43 | }), 44 | // setup fill styling below 45 | func(opt *charts.ChartOption) { 46 | opt.FillArea = charts.Ptr(true) // shade the area under the line 47 | opt.FillOpacity = 150 // set the fill opacity a little lighter than default 48 | opt.XAxis.BoundaryGap = charts.Ptr(false) // BoundaryGap is less appealing when enabling FillArea 49 | }, 50 | ) 51 | if err != nil { 52 | panic(err) 53 | } else if buf, err := p.Bytes(); err != nil { 54 | panic(err) 55 | } else if err = writeFile(buf); err != nil { 56 | panic(err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/multiple_charts-2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example of putting two charts together. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "multiple-charts-2.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {120, 132, 101, 134, 90, 230, 210}, 27 | {150, 232, 201, 154, 190, 330, 410}, 28 | {320, 332, 301, 334, 390, 330, 320}, 29 | } 30 | p, err := charts.LineRender( 31 | values, 32 | charts.XAxisLabelsOptionFunc([]string{ 33 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 34 | }), 35 | charts.LegendOptionFunc(charts.LegendOption{ 36 | SeriesNames: []string{ 37 | "Email", "Video Ads", "Direct", 38 | }, 39 | OverlayChart: charts.Ptr(false), 40 | Offset: charts.OffsetStr{ 41 | Top: charts.PositionBottom, 42 | Left: "20%", 43 | }, 44 | }), 45 | func(opt *charts.ChartOption) { 46 | opt.YAxis = []charts.YAxisOption{ 47 | { 48 | Max: charts.Ptr(2000.0), 49 | }, 50 | } 51 | opt.Symbol = charts.SymbolCircle 52 | opt.LineStrokeWidth = 1.2 53 | opt.ValueFormatter = func(f float64) string { 54 | return charts.FormatValueHumanize(f, 1, true) 55 | } 56 | 57 | opt.Children = []charts.ChartOption{ 58 | { 59 | Box: charts.NewBox(200, 10, 500, 200), 60 | SeriesList: charts.NewSeriesListHorizontalBar([][]float64{ 61 | {70, 90, 110, 130}, 62 | {80, 100, 120, 140}, 63 | }).ToGenericSeriesList(), 64 | Legend: charts.LegendOption{ 65 | SeriesNames: []string{ 66 | "2011", "2012", 67 | }, 68 | }, 69 | YAxis: []charts.YAxisOption{ 70 | { 71 | Labels: []string{ 72 | "USA", "India", "China", "World", 73 | }, 74 | }, 75 | }, 76 | }, 77 | } 78 | }, 79 | ) 80 | if err != nil { 81 | panic(err) 82 | } else if buf, err := p.Bytes(); err != nil { 83 | panic(err) 84 | } else if err = writeFile(buf); err != nil { 85 | panic(err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/pie_chart-1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example pie chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "pie-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := []float64{ 26 | 1048, 735, 580, 484, 300, 27 | } 28 | p, err := charts.PieRender( 29 | values, 30 | charts.TitleOptionFunc(charts.TitleOption{ 31 | Text: "Pie Chart", 32 | Subtext: "(Fake Data)", 33 | Offset: charts.OffsetCenter, 34 | FontStyle: charts.NewFontStyleWithSize(16), 35 | SubtextFontStyle: charts.NewFontStyleWithSize(10), 36 | }), 37 | charts.PaddingOptionFunc(charts.NewBoxEqual(20)), 38 | charts.LegendOptionFunc(charts.LegendOption{ 39 | SeriesNames: []string{ 40 | "Search Engine", "Direct", "Email", "Union Ads", "Video Ads", 41 | }, 42 | Vertical: charts.Ptr(true), 43 | Offset: charts.OffsetStr{ 44 | Left: "80%", 45 | Top: charts.PositionBottom, 46 | }, 47 | FontStyle: charts.NewFontStyleWithSize(10), 48 | }), 49 | ) 50 | if err != nil { 51 | panic(err) 52 | } else if buf, err := p.Bytes(); err != nil { 53 | panic(err) 54 | } else if err = writeFile(buf); err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/radar_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/go-analyze/charts" 8 | ) 9 | 10 | /* 11 | Example radar chart with a variety of basic configuration options shown. 12 | */ 13 | 14 | func writeFile(buf []byte) error { 15 | tmpPath := "./tmp" 16 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 17 | return err 18 | } 19 | 20 | file := filepath.Join(tmpPath, "radar-chart-1-basic.png") 21 | return os.WriteFile(file, buf, 0600) 22 | } 23 | 24 | func main() { 25 | values := [][]float64{ 26 | {4200, 3000, 20000, 35000, 50000, 18000}, 27 | {5000, 14000, 28000, 26000, 42000, 21000}, 28 | } 29 | p, err := charts.RadarRender( 30 | values, 31 | charts.TitleOptionFunc(charts.TitleOption{ 32 | Text: "Basic Radar Chart", 33 | FontStyle: charts.NewFontStyleWithSize(16), 34 | }), 35 | charts.LegendOptionFunc(charts.LegendOption{ 36 | SeriesNames: []string{ 37 | "Allocated Budget", "Actual Spending", 38 | }, 39 | Offset: charts.OffsetRight, 40 | }), 41 | charts.RadarIndicatorOptionFunc([]string{ 42 | "Sales", 43 | "Administration", 44 | "Information Technology", 45 | "Customer Support", 46 | "Development", 47 | "Marketing", 48 | }, []float64{ 49 | 6500, 16000, 30000, 38000, 52000, 25000, 50 | }), 51 | ) 52 | if err != nil { 53 | panic(err) 54 | } else if buf, err := p.Bytes(); err != nil { 55 | panic(err) 56 | } else if err = writeFile(buf); err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/2-OptionFunc/scatter_chart-1-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-analyze/charts" 9 | ) 10 | 11 | /* 12 | Example basic scatter chart with a variety of basic configuration options shown. 13 | */ 14 | 15 | func writeFile(buf []byte) error { 16 | tmpPath := "./tmp" 17 | if err := os.MkdirAll(tmpPath, 0700); err != nil { 18 | return err 19 | } 20 | 21 | file := filepath.Join(tmpPath, "scatter-chart-1-basic.png") 22 | return os.WriteFile(file, buf, 0600) 23 | } 24 | 25 | func main() { 26 | values := [][]float64{ 27 | {120, 132, 101, charts.GetNullValue(), 90, 230, 210}, 28 | {220, 182, 191, 234, 290, 330, 310}, 29 | {150, 232, 201, 154, 190, 330, 410}, 30 | {320, 332, 301, 334, 390, 330, 320}, 31 | {820, 932, 901, 934, 1290, 1330, 1320}, 32 | } 33 | p, err := charts.ScatterRender( 34 | values, 35 | charts.TitleTextOptionFunc("Scatter"), 36 | charts.XAxisLabelsOptionFunc([]string{ 37 | "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", 38 | }), 39 | charts.LegendLabelsOptionFunc([]string{ 40 | "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", 41 | }), 42 | func(opt *charts.ChartOption) { 43 | opt.Title.FontStyle.FontSize = 16 44 | opt.Legend.Padding = charts.Box{ 45 | Left: 100, 46 | } 47 | opt.Symbol = charts.SymbolCircle 48 | opt.ValueFormatter = func(f float64) string { 49 | return fmt.Sprintf("%.0f", f) 50 | } 51 | }, 52 | ) 53 | if err != nil { 54 | panic(err) 55 | } else if buf, err := p.Bytes(); err != nil { 56 | panic(err) 57 | } else if err = writeFile(buf); err != nil { 58 | panic(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /font.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "github.com/golang/freetype/truetype" 5 | 6 | "github.com/go-analyze/charts/chartdraw" 7 | ) 8 | 9 | const defaultFontSize = 12.0 10 | 11 | // InstallFont installs the font for charts 12 | func InstallFont(fontFamily string, data []byte) error { 13 | return chartdraw.InstallFont(fontFamily, data) 14 | } 15 | 16 | func getPreferredFont(fonts ...*truetype.Font) *truetype.Font { 17 | for _, font := range fonts { 18 | if font != nil { 19 | return font 20 | } 21 | } 22 | return GetDefaultFont() 23 | } 24 | 25 | // GetDefaultFont get default font. 26 | func GetDefaultFont() *truetype.Font { 27 | return chartdraw.GetDefaultFont() 28 | } 29 | 30 | // SetDefaultFont set default font by name. 31 | func SetDefaultFont(fontFamily string) error { 32 | return chartdraw.SetDefaultFont(fontFamily) 33 | } 34 | 35 | // GetFont get the font by font family or the default if the family is not installed. 36 | func GetFont(fontFamily string) *truetype.Font { 37 | return chartdraw.GetFont(fontFamily) 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-analyze/charts 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1 7 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 8 | github.com/stretchr/testify v1.10.0 9 | golang.org/x/image v0.24.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/pretty v0.2.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 7 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 8 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 15 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 16 | golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= 17 | golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 20 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /mark_point_test.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMarkPoint(t *testing.T) { 11 | t.Parallel() 12 | 13 | tests := []struct { 14 | render func(*Painter) ([]byte, error) 15 | result string 16 | }{ 17 | { 18 | render: func(p *Painter) ([]byte, error) { 19 | markPoint := newMarkPointPainter(p) 20 | markPoint.add(markPointRenderOption{ 21 | fillColor: ColorBlack, 22 | seriesValues: []float64{1, 2, 3}, 23 | markpoints: NewSeriesMarkList(SeriesMarkTypeMax), 24 | points: []Point{ 25 | {X: 10, Y: 10}, 26 | {X: 30, Y: 30}, 27 | {X: 50, Y: 50}, 28 | }, 29 | }) 30 | if _, err := markPoint.Render(); err != nil { 31 | return nil, err 32 | } 33 | return p.Bytes() 34 | }, 35 | result: "3", 36 | }, 37 | } 38 | 39 | for i, tt := range tests { 40 | t.Run(strconv.Itoa(i), func(t *testing.T) { 41 | p := NewPainter(PainterOptions{ 42 | OutputFormat: ChartOutputSVG, 43 | Width: 600, 44 | Height: 400, 45 | }, PainterThemeOption(GetTheme(ThemeLight))) 46 | data, err := tt.render(p.Child(PainterPaddingOption(NewBoxEqual(20)))) 47 | require.NoError(t, err) 48 | assertEqualSVG(t, tt.result, data) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /series_label_test.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLabelFormatPie(t *testing.T) { 10 | t.Parallel() 11 | 12 | assert.Equal(t, "a: 12%", 13 | labelFormatPie([]string{"a", "b"}, "", nil, 0, 10, 0.12)) 14 | 15 | assert.Equal(t, "b: 25%", 16 | labelFormatPie([]string{"a", "b"}, "", nil, 1, 20, 0.25)) 17 | 18 | assert.Equal(t, "a: f", 19 | labelFormatPie([]string{"a", "b"}, "{b}: {c}", func(f float64) string { 20 | return "f" 21 | }, 0, 10, 0.12)) 22 | } 23 | 24 | func TestLabelFormatFunnel(t *testing.T) { 25 | t.Parallel() 26 | 27 | assert.Equal(t, "a(12%)", 28 | labelFormatFunnel([]string{"a", "b"}, "", nil, 0, 10, 0.12)) 29 | 30 | assert.Equal(t, "b(25%)", 31 | labelFormatFunnel([]string{"a", "b"}, "", nil, 1, 20, 0.25)) 32 | 33 | assert.Equal(t, "b(f, 25%)", 34 | labelFormatFunnel([]string{"a", "b"}, "{b}({c}, {d})", func(f float64) string { 35 | return "f" 36 | }, 1, 20, 0.25)) 37 | } 38 | 39 | func TestLabelFormatter(t *testing.T) { 40 | t.Parallel() 41 | 42 | assert.Equal(t, "10", 43 | labelFormatValue([]string{"a", "b"}, "", nil, 0, 10, 0.12)) 44 | 45 | assert.Equal(t, "f f 12%", 46 | labelFormatValue([]string{"a", "b"}, "{c} {c} {d}", 47 | func(f float64) string { 48 | return "f" 49 | }, 50 | 0, 10, 0.12)) 51 | 52 | assert.Equal(t, "Name: a, Value: 10, Percent: 12%", 53 | labelFormatPie([]string{"a", "b"}, "Name: {b}, Value: {c}, Percent: {d}", nil, 54 | 0, 10, 0.12)) 55 | 56 | assert.Equal(t, "Name: b, Value: 20, Percent: 25%", 57 | labelFormatPie([]string{"a", "b"}, "Name: {b}, Value: {c}, Percent: {d}", nil, 58 | 1, 20, 0.25)) 59 | 60 | assert.Equal(t, "Empty Series '' 20", 61 | labelFormatPie([]string{}, "Empty Series '{b}' {c}", nil, 62 | 1, 20, 0.25)) 63 | } 64 | --------------------------------------------------------------------------------