diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d620e62 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: build on tag + +on: + push: + branches: [ web ] + pull_request: + branches: [ web ] + +jobs: + docker: + runs-on: ubuntu-latest + name: Build + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + - name: Set output + id: vars + run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + platforms: linux/amd64, linux/arm64 + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/go-charts + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index ce56fe7..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - - build: - name: Build - runs-on: ubuntu-latest - strategy: - matrix: - go: - - '1.22' - - '1.21' - - '1.20' - - '1.19' - - '1.18' - - '1.17' - steps: - - - name: Go ${{ matrix.go }} test - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go }} - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Get dependencies - run: - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest - - - name: Lint - run: make lint - - - name: Test - run: make test - - - name: Bench - run: make bench diff --git a/.gitignore b/.gitignore index 57206ee..2ac8a25 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ *.png -*.svg -tmp -NotoSansSC.ttf -.vscode \ No newline at end of file +*.svg \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3fdd253 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ + +FROM golang:1.19-alpine as builder + +ADD ./ /go-charts + +RUN apk update \ + && apk add docker git gcc make \ + && cd /go-charts \ + && make build + +FROM alpine + +EXPOSE 7001 + +COPY --from=builder /go-charts/go-charts /usr/local/bin/go-charts +COPY --from=builder /go-charts/entrypoint.sh /entrypoint.sh + + +CMD ["go-charts"] + +ENTRYPOINT ["/entrypoint.sh"] + +HEALTHCHECK --timeout=10s --interval=10s CMD [ "wget", "http://127.0.0.1:7001/ping", "-q", "-O", "-"] \ No newline at end of file diff --git a/Makefile b/Makefile index 7b718c4..22af142 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,9 @@ export GO111MODULE = on -.PHONY: default test test-cover dev hooks +.PHONY: default test test-cover dev build +build: + go build -tags netgo -o go-charts -# for test -test: - go test -race -cover ./... - -test-cover: - go test -race -coverprofile=test.out ./... && go tool cover --html=test.out - -bench: - go test --benchmem -bench=. ./... - -lint: - golangci-lint run - -hooks: - cp hooks/* .git/hooks/ \ No newline at end of file +release: + go mod tidy diff --git a/README.md b/README.md deleted file mode 100644 index 0650395..0000000 --- a/README.md +++ /dev/null @@ -1,542 +0,0 @@ -# go-charts - -Clone from https://github.com/vicanso/go-charts - -[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) -[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) - -[中文](./README_zh.md) - -`go-charts` base on [go-chart](https://github.com/wcharczuk/go-chart),it is simpler way for generating charts, which supports `svg` and `png` format and themes: `light`, `dark`, `grafana` and `ant`. The default format is `png` and the default theme is `light`. - -`Apache ECharts` is popular among Front-end developers, so `go-charts` supports the option of `Apache ECharts`. Developers can generate charts almost the same as `Apache ECharts`. - -Screenshot of common charts, the left part is light theme, the right part is grafana theme. - -

- go-charts -

- -

- go-table -

- -## Chart Type - -These chart types are supported: `line`, `bar`, `horizontal bar`, `pie`, `radar` or `funnel` and `table`. - -## Example - -More examples can be found in the [./examples/](./examples/) directory. - - -### Line Chart -```go -package main - -import ( - charts "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - { - // snip... - }, - { - // snip... - }, - { - // snip... - }, - { - // snip... - }, - } - p, err := charts.LineRender( - values, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, charts.PositionCenter), - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Bar Chart - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := [][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - // snip... - }, - } - p, err := charts.BarRender( - values, - charts.XAxisDataOptionFunc([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - charts.LegendLabelsOptionFunc([]string{ - "Rainfall", - "Evaporation", - }, charts.PositionRight), - charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage), - charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin), - // custom option func - func(opt *charts.ChartOption) { - opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ) - opt.SeriesList[1].MarkLine = charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ) - }, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Horizontal Bar Chart - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := [][]float64{ - { - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, - }, - { - // snip... - }, - } - p, err := charts.HorizontalBarRender( - values, - charts.TitleTextOptionFunc("World Population"), - charts.PaddingOptionFunc(charts.Box{ - Top: 20, - Right: 40, - Bottom: 20, - Left: 20, - }), - charts.LegendLabelsOptionFunc([]string{ - "2011", - "2012", - }), - charts.YAxisDataOptionFunc([]string{ - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World", - }), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Pie Chart - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := []float64{ - 1048, - 735, - 580, - 484, - 300, - } - p, err := charts.PieRender( - values, - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - Left: charts.PositionCenter, - }), - charts.PaddingOptionFunc(charts.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }), - charts.LegendOptionFunc(charts.LegendOption{ - Orient: charts.OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: charts.PositionLeft, - }), - charts.PieSeriesShowLabel(), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Radar Chart - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := [][]float64{ - { - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }, - { - // snip... - }, - } - p, err := charts.RadarRender( - values, - charts.TitleTextOptionFunc("Basic Radar Chart"), - charts.LegendLabelsOptionFunc([]string{ - "Allocated Budget", - "Actual Spending", - }), - charts.RadarIndicatorOptionFunc([]string{ - "Sales", - "Administration", - "Information Technology", - "Customer Support", - "Development", - "Marketing", - }, []float64{ - 6500, - 16000, - 30000, - 38000, - 52000, - 25000, - }), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Funnel Chart - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - values := []float64{ - 100, - 80, - 60, - 40, - 20, - } - p, err := charts.FunnelRender( - values, - charts.TitleTextOptionFunc("Funnel"), - charts.LegendLabelsOptionFunc([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### Table - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - header := []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - } - data := [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - } - spans := map[int]int{ - 0: 2, - 1: 1, - // 设置第三列的span - 2: 3, - 3: 2, - 4: 2, - } - p, err := charts.TableRender( - header, - data, - spans, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - // snip... -} -``` - -### ECharts Render - -```go -package main - -import ( - "git.smarteching.com/zeni/go-charts/v2" -) - -func main() { - buf, err := charts.RenderEChartsToPNG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) - // snip... -} -``` - -## ECharts Option - -The name with `[]` is new parameter, others are the same as `echarts`. - -- `[type]` The canvas type, support `svg` and `png`, default is `svg` -- `[theme]` The theme, support `dark`, `light` and `grafana`, default is `light` -- `[fontFamily]` The font family for chart -- `[padding]` The padding of chart -- `[box]` The canvas box of chart -- `[width]` The width of chart -- `[height]` The height of chart -- `title` Title component, including main title and subtitle - - `title.text` The main title text, supporting for \n for newlines - - `title.subtext`Subtitle text, supporting for \n for newlines - - `title.left` Distance between title component and the left side of the container. Left value can be instant pixel value like 20; it can also be a percentage value relative to container width like '20%'; and it can also be 'left', 'center', or 'right'. - - `title.top` Distance between title component and the top side of the container. Top value can be instant pixel value like 20 - - `title.textStyle.color` Text color for title - - `title.textStyle.fontSize` Text font size for title - - `title.textStyle.fontFamily` Text font family for title, it will change the font family for chart -- `xAxis` The x axis in cartesian(rectangular) coordinate. `go-charts` only support one x axis. - - `xAxis.boundaryGap` The boundary gap on both sides of a coordinate axis. The setting and behavior of category axes and non-category axes are different. If set `null` or `true`, the label appear in the center part of two axis ticks. - - `xAxis.splitNumber` Number of segments that the axis is split into. Note that this number serves only as a recommendation, and the true segments may be adjusted based on readability - - `xAxis.data` Category data, only support string array. -- `yAxis` The y axis in cartesian(rectangular) coordinate, it support 2 y axis - - `yAxis.min` The minimum value of axis. It will be automatically computed to make sure axis tick is equally distributed when not set - - `yAxis.max` The maximum value of axis. It will be automatically computed to make sure axis tick is equally distributed when not se. - - `yAxis.axisLabel.formatter` Formatter of axis label, which supports string template: `"formatter": "{value} kg"` - - `yAxis.axisLine.lineStyle.color` The color for line -- `legend` Legend component - - `legend.show` Whether to show legend - - `legend.data` Data array of legend, only support string array: ["Email", "Video Ads"] - - `legend.align` Legend marker and text aligning. Support `left` and `right`, default is `left` - - `legend.padding` legend space around content - - `legend.left` Distance between legend component and the left side of the container. Left value can be instant pixel value like 20; it can also be a percentage value relative to container width like '20%'; and it can also be 'left', 'center', or 'right'. - - `legend.top` Distance between legend component and the top side of the container. Top value can be instant pixel value like 20 -- `radar` Coordinate for radar charts - - `radar.indicator` Indicator of radar chart, which is used to assign multiple variables(dimensions) in radar chart - - `radar.indicator.name` Indicator's name - - `radar.indicator.max` The maximum value of indicator - - `radar.indicator.min` The minimum value of indicator, default value is 0. -- `series` The series for chart - - `series.name` Series name used for displaying in legend. - - `series.type` Series type: `line`, `bar`, `pie`, `radar` or `funnel` - - `series.radius` Radius of Pie chart:`50%`, default is `40%` - - `series.yAxisIndex` Index of y axis to combine with, which is useful for multiple y axes in one chart - - `series.label.show` Whether to show label - - `series.label.distance` Distance to the host graphic element - - `series.label.color` Label color - - `series.itemStyle.color` Color for the series's item - - `series.markPoint` Mark point in a chart. - - `series.markPoint.symbolSize` Symbol size, default is `30` - - `series.markPoint.data` Data array for mark points, each of which is an object and the type only support `max` and `min`: `[{"type": "max"}, {"type": "min"}]` - - `series.markLine` Mark line in a chart - - `series.markPoint.data` Data array for mark points, each of which is an object and the type only support `max`, `min` and `average`: `[{"type": "max"}, {"type": "min"}, {"type": "average"}]`` - - `series.data` Data array of series, which can be in the following forms: - - `value` It's a float array: [1.1, 2,3, 5.2] - - `object` It's a object value array: [{"value": 1048, "name": "Search Engine"},{"value": 735,"name": "Direct"}] -- `[children]` The options of children chart - - -## Performance - -Generate a png chart will be less than 20ms. It's better than using `chrome headless` with `echarts`. - -```bash -BenchmarkMultiChartPNGRender-8 78 15216336 ns/op 2298308 B/op 1148 allocs/op -BenchmarkMultiChartSVGRender-8 367 3356325 ns/op 20597282 B/op 3088 allocs/op -``` diff --git a/README_zh.md b/README_zh.md deleted file mode 100644 index 3f35b97..0000000 --- a/README_zh.md +++ /dev/null @@ -1,576 +0,0 @@ -# go-charts - -[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vicanso/go-charts/blob/master/LICENSE) -[![Build Status](https://github.com/vicanso/go-charts/workflows/Test/badge.svg)](https://github.com/vicanso/go-charts/actions) - -`go-charts`基于[go-chart](https://github.com/wcharczuk/go-chart),更简单方便的形式生成数据图表,支持`svg`与`png`两种方式的输出,支持主题`light`, `dark`, `grafana`以及`ant`。默认的输入格式为`png`,默认主题为`light`。 - -`Apache ECharts`在前端开发中得到众多开发者的认可,因此`go-charts`提供了兼容`Apache ECharts`的配置参数,简单快捷的生成相似的图表(`svg`或`png`),方便插入至Email或分享使用。下面为常用的图表截图(主题为light与grafana): - - -

- go-charts -

- -

- go-table -

0 { - p.Child(PainterPaddingOption(Box{ - Top: ticksPaddingTop, - Left: ticksPaddingLeft, - })).Ticks(TicksOption{ - Count: tickCount, - Length: tickLength, - Unit: unit, - Orient: orient, - First: opt.FirstAxis, - }) - p.LineStroke([]Point{ - { - X: x0, - Y: y0, - }, - { - X: x1, - Y: y1, - }, - }) - } - - p.Child(PainterPaddingOption(Box{ - Left: labelPaddingLeft, - Top: labelPaddingTop, - Right: labelPaddingRight, - })).MultiText(MultiTextOption{ - First: opt.FirstAxis, - Align: textAlign, - TextList: data, - Orient: orient, - Unit: unit, - Position: labelPosition, - TextRotation: opt.TextRotation, - Offset: opt.LabelOffset, - }) - // 显示辅助线 - if opt.SplitLineShow { - style.StrokeColor = opt.SplitLineColor - style.StrokeWidth = 1 - top.OverrideDrawingStyle(style) - if isVertical { - x0 := p.Width() - x1 := top.Width() - if opt.Position == PositionRight { - x0 = 0 - x1 = top.Width() - p.Width() - } - yValues := autoDivide(height, tickCount) - yValues = yValues[0 : len(yValues)-1] - for _, y := range yValues { - top.LineStroke([]Point{ - { - X: x0, - Y: y, - }, - { - X: x1, - Y: y, - }, - }) - } - } else { - y0 := p.Height() - defaultXAxisHeight - y1 := top.Height() - defaultXAxisHeight - for index, x := range autoDivide(width, tickCount) { - if index == 0 { - continue - } - top.LineStroke([]Point{ - { - X: x, - Y: y0, - }, - { - X: x, - Y: y1, - }, - }) - } - } - } - - return Box{ - Bottom: height, - Right: width, - }, nil -} diff --git a/axis_test.go b/axis_test.go deleted file mode 100644 index 85e18ca..0000000 --- a/axis_test.go +++ /dev/null @@ -1,173 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestAxis(t *testing.T) { - assert := assert.New(t) - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - // 底部x轴 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack, - }).Render() - return p.Bytes() - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 底部x轴文本居左 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - BoundaryGap: FalseFlag(), - }).Render() - return p.Bytes() - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 左侧y轴 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - Position: PositionLeft, - }).Render() - return p.Bytes() - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 左侧y轴居中 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - Position: PositionLeft, - BoundaryGap: FalseFlag(), - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack, - }).Render() - return p.Bytes() - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 右侧 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - Position: PositionRight, - BoundaryGap: FalseFlag(), - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack, - }).Render() - return p.Bytes() - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 顶部 - { - render: func(p *Painter) ([]byte, error) { - _, _ = NewAxisPainter(p, AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - Formatter: "{value} --", - Position: PositionTop, - }).Render() - return p.Bytes() - }, - result: "\\nMon --Tue --Wed --Thu --Fri --Sat --Sun --", - }, - } - - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/bar_chart.go b/bar_chart.go deleted file mode 100644 index 043e044..0000000 --- a/bar_chart.go +++ /dev/null @@ -1,253 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type barChart struct { - p *Painter - opt *BarChartOption -} - -// NewBarChart returns a bar chart renderer -func NewBarChart(p *Painter, opt BarChartOption) *barChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &barChart{ - p: p, - opt: &opt, - } -} - -type BarChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The x axis option - XAxis XAxisOption - // The padding of line chart - Padding Box - // The y axis option - YAxisOptions []YAxisOption - // The option of title - Title TitleOption - // The legend option - Legend LegendOption - BarWidth int - // Margin of bar - BarMargin int -} - -func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - p := b.p - opt := b.opt - seriesPainter := result.seriesPainter - - xRange := NewRange(AxisRangeOption{ - Painter: b.p, - DivideCount: len(opt.XAxis.Data), - Size: seriesPainter.Width(), - }) - x0, x1 := xRange.GetRange(0) - width := int(x1 - x0) - // 每一块之间的margin - margin := 10 - // 每一个bar之间的margin - barMargin := 5 - if width < 20 { - margin = 2 - barMargin = 2 - } else if width < 50 { - margin = 5 - barMargin = 3 - } - if opt.BarMargin > 0 { - barMargin = opt.BarMargin - } - seriesCount := len(seriesList) - // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / seriesCount - if opt.BarWidth > 0 && opt.BarWidth < barWidth { - barWidth = opt.BarWidth - // 重新计算margin - margin = (width - seriesCount*barWidth - barMargin*(seriesCount-1)) / 2 - } - barMaxHeight := seriesPainter.Height() - theme := opt.Theme - seriesNames := seriesList.Names() - - markPointPainter := NewMarkPointPainter(seriesPainter) - markLinePainter := NewMarkLinePainter(seriesPainter) - rendererList := []Renderer{ - markPointPainter, - markLinePainter, - } - for index := range seriesList { - series := seriesList[index] - yRange := result.axisRanges[series.AxisIndex] - seriesColor := theme.GetSeriesColor(series.index) - - divideValues := xRange.AutoDivide() - points := make([]Point, len(series.Data)) - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } - - for j, item := range series.Data { - if j >= xRange.divideCount { - continue - } - x := divideValues[j] - x += margin - if index != 0 { - x += index * (barWidth + barMargin) - } - - h := int(yRange.getHeight(item.Value)) - fillColor := seriesColor - if !item.Style.FillColor.IsZero() { - fillColor = item.Style.FillColor - } - top := barMaxHeight - h - - if series.RoundRadius <= 0 { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }) - } else { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).RoundedRect(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }, series.RoundRadius) - } - // 用于生成marker point - points[j] = Point{ - // 居中的位置 - X: x + barWidth>>1, - Y: top, - } - // 用于生成marker point - points[j] = Point{ - // 居中的位置 - X: x + barWidth>>1, - Y: top, - } - // 如果label不需要展示,则返回 - if labelPainter == nil { - continue - } - y := barMaxHeight - h - radians := float64(0) - fontColor := series.Label.Color - if series.Label.Position == PositionBottom { - y = barMaxHeight - radians = -math.Pi / 2 - if fontColor.IsZero() { - if isLightColor(fillColor) { - fontColor = defaultLightFontColor - } else { - fontColor = defaultDarkFontColor - } - } - } - labelPainter.Add(LabelValue{ - Index: index, - Value: item.Value, - X: x + barWidth>>1, - Y: y, - // 旋转 - Radians: radians, - FontColor: fontColor, - Offset: series.Label.Offset, - FontSize: series.Label.FontSize, - }) - } - - markPointPainter.Add(markPointRenderOption{ - FillColor: seriesColor, - Font: opt.Font, - Series: series, - Points: points, - }) - markLinePainter.Add(markLineRenderOption{ - FillColor: seriesColor, - FontColor: opt.Theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: series, - Range: yRange, - }) - } - // 最大、最小的mark point - err := doRender(rendererList...) - if err != nil { - return BoxZero, err - } - - return p.box, nil -} - -func (b *barChart) Render() (Box, error) { - p := b.p - opt := b.opt - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypeLine) - return b.render(renderResult, seriesList) -} diff --git a/bar_chart_test.go b/bar_chart_test.go deleted file mode 100644 index 654c320..0000000 --- a/bar_chart_test.go +++ /dev/null @@ -1,190 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBarChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - seriesList := NewSeriesListDataFromValues([][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }, - }) - for index := range seriesList { - seriesList[index].Label.Show = true - } - _, err := NewBarChart(p, BarChartOption{ - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - SeriesList: seriesList, - XAxis: NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - YAxisOptions: NewYAxisOptions([]string{ - "Rainfall", - "Evaporation", - }), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", - }, - { - render: func(p *Painter) ([]byte, error) { - seriesList := NewSeriesListDataFromValues([][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }, - }) - for index := range seriesList { - seriesList[index].Label.Show = true - seriesList[index].RoundRadius = 5 - } - _, err := NewBarChart(p, BarChartOption{ - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - SeriesList: seriesList, - XAxis: NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - YAxisOptions: NewYAxisOptions([]string{ - "Rainfall", - "Evaporation", - }), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n24020016012080400FebMayAugNov24.9723.225.676.7135.6162.232.6206.43.32.65.9926.428.770.7175.6182.248.718.862.3", - }, - } - - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/chart_option.go b/chart_option.go deleted file mode 100644 index d80a383..0000000 --- a/chart_option.go +++ /dev/null @@ -1,426 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "sort" - - "github.com/golang/freetype/truetype" -) - -type ChartOption struct { - theme ColorPalette - font *truetype.Font - // The output type of chart, "svg" or "png", default value is "svg" - Type string - // The font family, which should be installed first - FontFamily string - // The theme of chart, "light" and "dark". - // The default theme is "light" - Theme string - // The title option - Title TitleOption - // The legend option - Legend LegendOption - // The x axis option - XAxis XAxisOption - // The y axis option list - YAxisOptions []YAxisOption - // The width of chart, default width is 600 - Width int - // The height of chart, default height is 400 - Height int - Parent *Painter - // The padding for chart, default padding is [20, 10, 10, 10] - Padding Box - // The canvas box for chart - Box Box - // The series list - SeriesList SeriesList - // The radar indicator list - RadarIndicators []RadarIndicator - // The background color of chart - BackgroundColor Color - // The flag for show symbol of line, set this to *false will hide symbol - SymbolShow *bool - // The stroke width of line chart - LineStrokeWidth float64 - // The bar with of bar chart - BarWidth int - // The margin of each bar - BarMargin int - // The bar height of horizontal bar chart - BarHeight int - // Fill the area of line chart - FillArea bool - // background fill (alpha) opacity - Opacity uint8 - // The child charts - Children []ChartOption - // The value formatter - ValueFormatter ValueFormatter -} - -// OptionFunc option function -type OptionFunc func(opt *ChartOption) - -// SVGTypeOption set svg type of chart's output -func SVGTypeOption() OptionFunc { - return TypeOptionFunc(ChartOutputSVG) -} - -// PNGTypeOption set png type of chart's output -func PNGTypeOption() OptionFunc { - return TypeOptionFunc(ChartOutputPNG) -} - -// TypeOptionFunc set type of chart's output -func TypeOptionFunc(t string) OptionFunc { - return func(opt *ChartOption) { - opt.Type = t - } -} - -// FontFamilyOptionFunc set font family of chart -func FontFamilyOptionFunc(fontFamily string) OptionFunc { - return func(opt *ChartOption) { - opt.FontFamily = fontFamily - } -} - -// ThemeOptionFunc set them of chart -func ThemeOptionFunc(theme string) OptionFunc { - return func(opt *ChartOption) { - opt.Theme = theme - } -} - -// TitleOptionFunc set title of chart -func TitleOptionFunc(title TitleOption) OptionFunc { - return func(opt *ChartOption) { - opt.Title = title - } -} - -// TitleTextOptionFunc set title text of chart -func TitleTextOptionFunc(text string, subtext ...string) OptionFunc { - return func(opt *ChartOption) { - opt.Title.Text = text - if len(subtext) != 0 { - opt.Title.Subtext = subtext[0] - } - } -} - -// LegendOptionFunc set legend of chart -func LegendOptionFunc(legend LegendOption) OptionFunc { - return func(opt *ChartOption) { - opt.Legend = legend - } -} - -// LegendLabelsOptionFunc set legend labels of chart -func LegendLabelsOptionFunc(labels []string, left ...string) OptionFunc { - return func(opt *ChartOption) { - opt.Legend = NewLegendOption(labels, left...) - } -} - -// XAxisOptionFunc set x axis of chart -func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.XAxis = xAxisOption - } -} - -// XAxisDataOptionFunc set x axis data of chart -func XAxisDataOptionFunc(data []string, boundaryGap ...*bool) OptionFunc { - return func(opt *ChartOption) { - opt.XAxis = NewXAxisOption(data, boundaryGap...) - } -} - -// YAxisOptionFunc set y axis of chart, support two y axis -func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.YAxisOptions = yAxisOption - } -} - -// YAxisDataOptionFunc set y axis data of chart -func YAxisDataOptionFunc(data []string) OptionFunc { - return func(opt *ChartOption) { - opt.YAxisOptions = NewYAxisOptions(data) - } -} - -// WidthOptionFunc set width of chart -func WidthOptionFunc(width int) OptionFunc { - return func(opt *ChartOption) { - opt.Width = width - } -} - -// HeightOptionFunc set height of chart -func HeightOptionFunc(height int) OptionFunc { - return func(opt *ChartOption) { - opt.Height = height - } -} - -// PaddingOptionFunc set padding of chart -func PaddingOptionFunc(padding Box) OptionFunc { - return func(opt *ChartOption) { - opt.Padding = padding - } -} - -// BoxOptionFunc set box of chart -func BoxOptionFunc(box Box) OptionFunc { - return func(opt *ChartOption) { - opt.Box = box - } -} - -// PieSeriesShowLabel set pie series show label -func PieSeriesShowLabel() OptionFunc { - return func(opt *ChartOption) { - for index := range opt.SeriesList { - opt.SeriesList[index].Label.Show = true - } - } -} - -// ChildOptionFunc add child chart -func ChildOptionFunc(child ...ChartOption) OptionFunc { - return func(opt *ChartOption) { - if opt.Children == nil { - opt.Children = make([]ChartOption, 0) - } - opt.Children = append(opt.Children, child...) - } -} - -// RadarIndicatorOptionFunc set radar indicator of chart -func RadarIndicatorOptionFunc(names []string, values []float64) OptionFunc { - return func(opt *ChartOption) { - opt.RadarIndicators = NewRadarIndicators(names, values) - } -} - -// BackgroundColorOptionFunc set background color of chart -func BackgroundColorOptionFunc(color Color) OptionFunc { - return func(opt *ChartOption) { - opt.BackgroundColor = color - } -} - -// MarkLineOptionFunc set mark line for series of chart -func MarkLineOptionFunc(seriesIndex int, markLineTypes ...string) OptionFunc { - return func(opt *ChartOption) { - if len(opt.SeriesList) <= seriesIndex { - return - } - opt.SeriesList[seriesIndex].MarkLine = NewMarkLine(markLineTypes...) - } -} - -// MarkPointOptionFunc set mark point for series of chart -func MarkPointOptionFunc(seriesIndex int, markPointTypes ...string) OptionFunc { - return func(opt *ChartOption) { - if len(opt.SeriesList) <= seriesIndex { - return - } - opt.SeriesList[seriesIndex].MarkPoint = NewMarkPoint(markPointTypes...) - } -} - -func (o *ChartOption) fillDefault() { - t := NewTheme(o.Theme) - o.theme = t - // 如果为空,初始化 - axisCount := 1 - for _, series := range o.SeriesList { - if series.AxisIndex >= axisCount { - axisCount++ - } - } - o.Width = getDefaultInt(o.Width, defaultChartWidth) - o.Height = getDefaultInt(o.Height, defaultChartHeight) - yAxisOptions := make([]YAxisOption, axisCount) - copy(yAxisOptions, o.YAxisOptions) - o.YAxisOptions = yAxisOptions - o.font, _ = GetFont(o.FontFamily) - - if o.font == nil { - o.font, _ = GetDefaultFont() - } else { - // 如果指定了字体,则设置主题的字体 - t.SetFont(o.font) - } - if o.BackgroundColor.IsZero() { - o.BackgroundColor = t.GetBackgroundColor() - } - if o.Padding.IsZero() { - o.Padding = Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - } - } - // legend与series name的关联 - if len(o.Legend.Data) == 0 { - o.Legend.Data = o.SeriesList.Names() - } else { - seriesCount := len(o.SeriesList) - for index, name := range o.Legend.Data { - if index < seriesCount && - len(o.SeriesList[index].Name) == 0 { - o.SeriesList[index].Name = name - } - } - nameIndexDict := map[string]int{} - for index, name := range o.Legend.Data { - nameIndexDict[name] = index - } - // 保证series的顺序与legend一致 - sort.Slice(o.SeriesList, func(i, j int) bool { - return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name] - }) - } -} - -// LineRender line chart render -func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { - seriesList := NewSeriesListDataFromValues(values, ChartTypeLine) - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// BarRender bar chart render -func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { - seriesList := NewSeriesListDataFromValues(values, ChartTypeBar) - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// HorizontalBarRender horizontal bar chart render -func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { - seriesList := NewSeriesListDataFromValues(values, ChartTypeHorizontalBar) - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// PieRender pie chart render -func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) { - return Render(ChartOption{ - SeriesList: NewPieSeriesList(values), - }, opts...) -} - -// RadarRender radar chart render -func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { - seriesList := NewSeriesListDataFromValues(values, ChartTypeRadar) - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// FunnelRender funnel chart render -func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { - seriesList := NewFunnelSeriesList(values) - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// TableRender table chart render -func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) { - opt := TableChartOption{ - Header: header, - Data: data, - } - if len(spanMaps) != 0 { - spanMap := spanMaps[0] - spans := make([]int, len(opt.Header)) - for index := range spans { - v, ok := spanMap[index] - if !ok { - v = 1 - } - spans[index] = v - } - opt.Spans = spans - } - return TableOptionRender(opt) -} - -// TableOptionRender table render with option -func TableOptionRender(opt TableChartOption) (*Painter, error) { - if opt.Type == "" { - opt.Type = ChartOutputPNG - } - if opt.Width <= 0 { - opt.Width = defaultChartWidth - } - if opt.FontFamily != "" { - opt.Font, _ = GetFont(opt.FontFamily) - } - if opt.Font == nil { - opt.Font, _ = GetDefaultFont() - } - - p, err := NewPainter(PainterOptions{ - Type: opt.Type, - Width: opt.Width, - // 仅用于计算表格高度,因此随便设置即可 - Height: 100, - Font: opt.Font, - }) - if err != nil { - return nil, err - } - info, err := NewTableChart(p, opt).render() - if err != nil { - return nil, err - } - - p, err = NewPainter(PainterOptions{ - Type: opt.Type, - Width: info.Width, - Height: info.Height, - Font: opt.Font, - }) - if err != nil { - return nil, err - } - _, err = NewTableChart(p, opt).renderWithInfo(info) - if err != nil { - return nil, err - } - return p, nil -} diff --git a/chart_option_test.go b/chart_option_test.go deleted file mode 100644 index c354b26..0000000 --- a/chart_option_test.go +++ /dev/null @@ -1,451 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestChartOption(t *testing.T) { - assert := assert.New(t) - - fns := []OptionFunc{ - SVGTypeOption(), - FontFamilyOptionFunc("fontFamily"), - ThemeOptionFunc("theme"), - TitleTextOptionFunc("title"), - LegendLabelsOptionFunc([]string{ - "label", - }), - XAxisDataOptionFunc([]string{ - "xaxis", - }), - YAxisDataOptionFunc([]string{ - "yaxis", - }), - WidthOptionFunc(800), - HeightOptionFunc(600), - PaddingOptionFunc(Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }), - BackgroundColorOptionFunc(drawing.ColorBlack), - } - opt := ChartOption{} - for _, fn := range fns { - fn(&opt) - } - assert.Equal(ChartOption{ - Type: ChartOutputSVG, - FontFamily: "fontFamily", - Theme: "theme", - Title: TitleOption{ - Text: "title", - }, - Legend: LegendOption{ - Data: []string{ - "label", - }, - }, - XAxis: XAxisOption{ - Data: []string{ - "xaxis", - }, - }, - YAxisOptions: []YAxisOption{ - { - Data: []string{ - "yaxis", - }, - }, - }, - Width: 800, - Height: 600, - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - BackgroundColor: drawing.ColorBlack, - }, opt) -} - -func TestChartOptionPieSeriesShowLabel(t *testing.T) { - assert := assert.New(t) - - opt := ChartOption{ - SeriesList: NewPieSeriesList([]float64{ - 1, - 2, - }), - } - PieSeriesShowLabel()(&opt) - assert.True(opt.SeriesList[0].Label.Show) -} - -func TestChartOptionMarkLine(t *testing.T) { - assert := assert.New(t) - opt := ChartOption{ - SeriesList: NewSeriesListDataFromValues([][]float64{ - {1, 2}, - }), - } - MarkLineOptionFunc(0, "min", "max")(&opt) - assert.Equal(NewMarkLine("min", "max"), opt.SeriesList[0].MarkLine) -} - -func TestChartOptionMarkPoint(t *testing.T) { - assert := assert.New(t) - opt := ChartOption{ - SeriesList: NewSeriesListDataFromValues([][]float64{ - {1, 2}, - }), - } - MarkPointOptionFunc(0, "min", "max")(&opt) - assert.Equal(NewMarkPoint("min", "max"), opt.SeriesList[0].MarkPoint) -} - -func TestLineRender(t *testing.T) { - assert := assert.New(t) - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - { - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }, - { - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }, - { - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }, - { - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }, - } - p, err := LineRender( - values, - SVGTypeOption(), - TitleTextOptionFunc("Line"), - XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, PositionCenter), - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", string(data)) -} - -func TestBarRender(t *testing.T) { - assert := assert.New(t) - values := [][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }, - } - p, err := BarRender( - values, - SVGTypeOption(), - XAxisDataOptionFunc([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - LegendLabelsOptionFunc([]string{ - "Rainfall", - "Evaporation", - }, PositionRight), - MarkLineOptionFunc(0, SeriesMarkDataTypeAverage), - MarkPointOptionFunc(0, SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin), - // custom option func - func(opt *ChartOption) { - opt.SeriesList[1].MarkPoint = NewMarkPoint( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - ) - opt.SeriesList[1].MarkLine = NewMarkLine( - SeriesMarkDataTypeAverage, - ) - }, - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\nRainfallEvaporation24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) -} - -func TestHorizontalBarRender(t *testing.T) { - assert := assert.New(t) - values := [][]float64{ - { - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, - }, - { - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, - }, - } - p, err := HorizontalBarRender( - values, - SVGTypeOption(), - TitleTextOptionFunc("World Population"), - PaddingOptionFunc(Box{ - Top: 20, - Right: 40, - Bottom: 20, - Left: 20, - }), - LegendLabelsOptionFunc([]string{ - "2011", - "2012", - }), - YAxisDataOptionFunc([]string{ - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World", - }), - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", string(data)) -} - -func TestPieRender(t *testing.T) { - assert := assert.New(t) - values := []float64{ - 1048, - 735, - 580, - 484, - 300, - } - p, err := PieRender( - values, - SVGTypeOption(), - TitleOptionFunc(TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - Left: PositionCenter, - }), - PaddingOptionFunc(Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }), - LegendOptionFunc(LegendOption{ - Orient: OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: PositionLeft, - }), - PieSeriesShowLabel(), - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", string(data)) -} - -func TestRadarRender(t *testing.T) { - assert := assert.New(t) - - values := [][]float64{ - { - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }, - { - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }, - } - p, err := RadarRender( - values, - SVGTypeOption(), - TitleTextOptionFunc("Basic Radar Chart"), - LegendLabelsOptionFunc([]string{ - "Allocated Budget", - "Actual Spending", - }), - RadarIndicatorOptionFunc([]string{ - "Sales", - "Administration", - "Information Technology", - "Customer Support", - "Development", - "Marketing", - }, []float64{ - 6500, - 16000, - 30000, - 38000, - 52000, - 25000, - }), - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) -} - -func TestFunnelRender(t *testing.T) { - assert := assert.New(t) - - values := []float64{ - 100, - 80, - 60, - 40, - 20, - } - p, err := FunnelRender( - values, - SVGTypeOption(), - TitleTextOptionFunc("Funnel"), - LegendLabelsOptionFunc([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - ) - assert.Nil(err) - data, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\nShowClickVisitInquiryOrderFunnelShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) -} diff --git a/charts.go b/charts.go deleted file mode 100644 index 31df11c..0000000 --- a/charts.go +++ /dev/null @@ -1,473 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "math" - "sort" - - "git.smarteching.com/zeni/go-chart/v2" -) - -const labelFontSize = 10 -const smallLabelFontSize = 8 -const defaultDotWidth = 2.0 -const defaultStrokeWidth = 2.0 - -var defaultChartWidth = 600 -var defaultChartHeight = 400 - -// SetDefaultWidth sets default width of chart -func SetDefaultWidth(width int) { - if width > 0 { - defaultChartWidth = width - } -} - -// SetDefaultHeight sets default height of chart -func SetDefaultHeight(height int) { - if height > 0 { - defaultChartHeight = height - } -} - -var nullValue = math.MaxFloat64 - -// SetNullValue sets the null value, default is MaxFloat64 -func SetNullValue(v float64) { - nullValue = v -} - -// GetNullValue gets the null value -func GetNullValue() float64 { - return nullValue -} - -type Renderer interface { - Render() (Box, error) -} - -type renderHandler struct { - list []func() error -} - -func (rh *renderHandler) Add(fn func() error) { - list := rh.list - if len(list) == 0 { - list = make([]func() error, 0) - } - rh.list = append(list, fn) -} - -func (rh *renderHandler) Do() error { - for _, fn := range rh.list { - err := fn() - if err != nil { - return err - } - } - return nil -} - -type defaultRenderOption struct { - Theme ColorPalette - Padding Box - SeriesList SeriesList - // The y axis option - YAxisOptions []YAxisOption - // The x axis option - XAxis XAxisOption - // The title option - TitleOption TitleOption - // The legend option - LegendOption LegendOption - // background is filled - backgroundIsFilled bool - // x y axis is reversed - axisReversed bool -} - -type defaultRenderResult struct { - axisRanges map[int]axisRange - // 图例区域 - seriesPainter *Painter -} - -func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { - seriesList := opt.SeriesList - seriesList.init() - if !opt.backgroundIsFilled { - p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) - } - - if !opt.Padding.IsZero() { - p = p.Child(PainterPaddingOption(opt.Padding)) - } - - legendHeight := 0 - if len(opt.LegendOption.Data) != 0 { - if opt.LegendOption.Theme == nil { - opt.LegendOption.Theme = opt.Theme - } - legendResult, err := NewLegendPainter(p, opt.LegendOption).Render() - if err != nil { - return nil, err - } - legendHeight = legendResult.Height() - } - - // 如果有标题 - if opt.TitleOption.Text != "" { - if opt.TitleOption.Theme == nil { - opt.TitleOption.Theme = opt.Theme - } - titlePainter := NewTitlePainter(p, opt.TitleOption) - - titleBox, err := titlePainter.Render() - if err != nil { - return nil, err - } - - top := chart.MaxInt(legendHeight, titleBox.Height()) - // 如果是垂直方式,则不计算legend高度 - if opt.LegendOption.Orient == OrientVertical { - top = titleBox.Height() - } - p = p.Child(PainterPaddingOption(Box{ - // 标题下留白 - Top: top + 20, - })) - } - - result := defaultRenderResult{ - axisRanges: make(map[int]axisRange), - } - - // 计算图表对应的轴有哪些 - axisIndexList := make([]int, 0) - for _, series := range opt.SeriesList { - if containsInt(axisIndexList, series.AxisIndex) { - continue - } - axisIndexList = append(axisIndexList, series.AxisIndex) - } - // 高度需要减去x轴的高度 - rangeHeight := p.Height() - defaultXAxisHeight - rangeWidthLeft := 0 - rangeWidthRight := 0 - - // 倒序 - sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList))) - - // 计算对应的axis range - for _, index := range axisIndexList { - yAxisOption := YAxisOption{} - if len(opt.YAxisOptions) > index { - yAxisOption = opt.YAxisOptions[index] - } - divideCount := yAxisOption.DivideCount - if divideCount <= 0 { - divideCount = defaultAxisDivideCount - } - max, min := opt.SeriesList.GetMaxMin(index) - r := NewRange(AxisRangeOption{ - Painter: p, - Min: min, - Max: max, - // 高度需要减去x轴的高度 - Size: rangeHeight, - // 分隔数量 - DivideCount: divideCount, - }) - if yAxisOption.Min != nil && *yAxisOption.Min <= min { - r.min = *yAxisOption.Min - } - if yAxisOption.Max != nil && *yAxisOption.Max >= max { - r.max = *yAxisOption.Max - } - result.axisRanges[index] = r - - if yAxisOption.Theme == nil { - yAxisOption.Theme = opt.Theme - } - if !opt.axisReversed { - yAxisOption.Data = r.Values() - } else { - yAxisOption.isCategoryAxis = true - // 由于x轴为value部分,因此计算其label单独处理 - opt.XAxis.Data = NewRange(AxisRangeOption{ - Painter: p, - Min: min, - Max: max, - // 高度需要减去x轴的高度 - Size: rangeHeight, - // 分隔数量 - DivideCount: defaultAxisDivideCount, - }).Values() - opt.XAxis.isValueAxis = true - } - reverseStringSlice(yAxisOption.Data) - // TODO生成其它位置既yAxis - var yAxis *axisPainter - child := p.Child(PainterPaddingOption(Box{ - Left: rangeWidthLeft, - Right: rangeWidthRight, - })) - if index == 0 { - yAxis = NewLeftYAxis(child, yAxisOption) - } else { - yAxis = NewRightYAxis(child, yAxisOption) - } - yAxisBox, err := yAxis.Render() - if err != nil { - return nil, err - } - if index == 0 { - rangeWidthLeft += yAxisBox.Width() - } else { - rangeWidthRight += yAxisBox.Width() - } - } - - if opt.XAxis.Theme == nil { - opt.XAxis.Theme = opt.Theme - } - xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{ - Left: rangeWidthLeft, - Right: rangeWidthRight, - })), opt.XAxis) - _, err := xAxis.Render() - if err != nil { - return nil, err - } - - result.seriesPainter = p.Child(PainterPaddingOption(Box{ - Bottom: defaultXAxisHeight, - Left: rangeWidthLeft, - Right: rangeWidthRight, - })) - return &result, nil -} - -func doRender(renderers ...Renderer) error { - for _, r := range renderers { - _, err := r.Render() - if err != nil { - return err - } - } - return nil -} - -func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { - for _, fn := range opts { - fn(&opt) - } - opt.fillDefault() - - isChild := true - if opt.Parent == nil { - isChild = false - p, err := NewPainter(PainterOptions{ - Type: opt.Type, - Width: opt.Width, - Height: opt.Height, - Font: opt.font, - }) - if err != nil { - return nil, err - } - opt.Parent = p - } - p := opt.Parent - if opt.ValueFormatter != nil { - p.valueFormatter = opt.ValueFormatter - } - if !opt.Box.IsZero() { - p = p.Child(PainterBoxOption(opt.Box)) - } - if !isChild { - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) - } - seriesList := opt.SeriesList - seriesList.init() - - seriesCount := len(seriesList) - - // line chart - lineSeriesList := seriesList.Filter(ChartTypeLine) - barSeriesList := seriesList.Filter(ChartTypeBar) - horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar) - pieSeriesList := seriesList.Filter(ChartTypePie) - radarSeriesList := seriesList.Filter(ChartTypeRadar) - funnelSeriesList := seriesList.Filter(ChartTypeFunnel) - - if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount { - return nil, errors.New("Horizontal bar can not mix other charts") - } - if len(pieSeriesList) != 0 && len(pieSeriesList) != seriesCount { - return nil, errors.New("Pie can not mix other charts") - } - if len(radarSeriesList) != 0 && len(radarSeriesList) != seriesCount { - return nil, errors.New("Radar can not mix other charts") - } - if len(funnelSeriesList) != 0 && len(funnelSeriesList) != seriesCount { - return nil, errors.New("Funnel can not mix other charts") - } - - axisReversed := len(horizontalBarSeriesList) != 0 - renderOpt := defaultRenderOption{ - Theme: opt.theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - axisReversed: axisReversed, - // 前置已设置背景色 - backgroundIsFilled: true, - } - if len(pieSeriesList) != 0 || - len(radarSeriesList) != 0 || - len(funnelSeriesList) != 0 { - renderOpt.XAxis.Show = FalseFlag() - renderOpt.YAxisOptions = []YAxisOption{ - { - Show: FalseFlag(), - }, - } - } - if len(horizontalBarSeriesList) != 0 { - renderOpt.YAxisOptions[0].DivideCount = len(renderOpt.YAxisOptions[0].Data) - renderOpt.YAxisOptions[0].Unit = 1 - } - - renderResult, err := defaultRender(p, renderOpt) - if err != nil { - return nil, err - } - - handler := renderHandler{} - - // bar chart - if len(barSeriesList) != 0 { - handler.Add(func() error { - _, err := NewBarChart(p, BarChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - BarWidth: opt.BarWidth, - BarMargin: opt.BarMargin, - }).render(renderResult, barSeriesList) - return err - }) - } - - // horizontal bar chart - if len(horizontalBarSeriesList) != 0 { - handler.Add(func() error { - _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ - Theme: opt.theme, - Font: opt.font, - BarHeight: opt.BarHeight, - BarMargin: opt.BarMargin, - YAxisOptions: opt.YAxisOptions, - }).render(renderResult, horizontalBarSeriesList) - return err - }) - } - - // pie chart - if len(pieSeriesList) != 0 { - handler.Add(func() error { - _, err := NewPieChart(p, PieChartOption{ - Theme: opt.theme, - Font: opt.font, - }).render(renderResult, pieSeriesList) - return err - }) - } - - // line chart - if len(lineSeriesList) != 0 { - handler.Add(func() error { - _, err := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - XAxis: opt.XAxis, - SymbolShow: opt.SymbolShow, - StrokeWidth: opt.LineStrokeWidth, - FillArea: opt.FillArea, - Opacity: opt.Opacity, - }).render(renderResult, lineSeriesList) - return err - }) - } - - // radar chart - if len(radarSeriesList) != 0 { - handler.Add(func() error { - _, err := NewRadarChart(p, RadarChartOption{ - Theme: opt.theme, - Font: opt.font, - // 相应值 - RadarIndicators: opt.RadarIndicators, - }).render(renderResult, radarSeriesList) - return err - }) - } - - // funnel chart - if len(funnelSeriesList) != 0 { - handler.Add(func() error { - _, err := NewFunnelChart(p, FunnelChartOption{ - Theme: opt.theme, - Font: opt.font, - }).render(renderResult, funnelSeriesList) - return err - }) - } - - err = handler.Do() - - if err != nil { - return nil, err - } - for _, item := range opt.Children { - item.Parent = p - if item.Theme == "" { - item.Theme = opt.Theme - } - if item.FontFamily == "" { - item.FontFamily = opt.FontFamily - } - _, err = Render(item) - if err != nil { - return nil, err - } - } - - return p, nil -} diff --git a/charts_test.go b/charts_test.go deleted file mode 100644 index bd581e9..0000000 --- a/charts_test.go +++ /dev/null @@ -1,255 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "testing" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func BenchmarkMultiChartPNGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Type: ChartOutputPNG, - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisOptions: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} - -func BenchmarkMultiChartSVGRender(b *testing.B) { - for i := 0; i < b.N; i++ { - opt := ChartOption{ - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisOptions: []YAxisOption{ - { - - Min: NewFloatPoint(0), - Max: NewFloatPoint(90), - }, - }, - SeriesList: []Series{ - NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, ChartTypeBar), - NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, ChartTypeBar), - }, - Children: []ChartOption{ - { - Legend: LegendOption{ - Show: FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - } - d, err := Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - if len(buf) == 0 { - panic(errors.New("data is nil")) - } - } -} diff --git a/echarts.go b/echarts.go deleted file mode 100644 index aaef1f1..0000000 --- a/echarts.go +++ /dev/null @@ -1,528 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "bytes" - "encoding/json" - "fmt" - "regexp" - "strconv" - - "git.smarteching.com/zeni/go-chart/v2" -) - -func convertToArray(data []byte) []byte { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if data[0] != '[' { - data = []byte("[" + string(data) + "]") - } - return data -} - -type EChartsPosition string - -func (p *EChartsPosition) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - return nil - } - if regexp.MustCompile(`^\d+`).Match(data) { - data = []byte(fmt.Sprintf(`"%s"`, string(data))) - } - s := (*string)(p) - return json.Unmarshal(data, s) -} - -type EChartStyle struct { - Color string `json:"color"` -} - -func (es *EChartStyle) ToStyle() Style { - color := parseColor(es.Color) - return Style{ - FillColor: color, - FontColor: color, - StrokeColor: color, - } -} - -type EChartsSeriesDataValue struct { - values []float64 -} - -func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - return json.Unmarshal(data, &value.values) -} -func (value *EChartsSeriesDataValue) First() float64 { - if len(value.values) == 0 { - return 0 - } - return value.values[0] -} -func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue { - return EChartsSeriesDataValue{ - values: values, - } -} - -type EChartsSeriesData struct { - Value EChartsSeriesDataValue `json:"value"` - Name string `json:"name"` - ItemStyle EChartStyle `json:"itemStyle"` -} -type _EChartsSeriesData EChartsSeriesData - -var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`) - -func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if numericRep.Match(data) { - v, err := strconv.ParseFloat(string(data), 64) - if err != nil { - return err - } - es.Value = EChartsSeriesDataValue{ - values: []float64{ - v, - }, - } - return nil - } - v := _EChartsSeriesData{} - err := json.Unmarshal(data, &v) - if err != nil { - return err - } - es.Name = v.Name - es.Value = v.Value - es.ItemStyle = v.ItemStyle - return nil -} - -type EChartsXAxisData struct { - BoundaryGap *bool `json:"boundaryGap"` - SplitNumber int `json:"splitNumber"` - Data []string `json:"data"` - Type string `json:"type"` -} -type EChartsXAxis struct { - Data []EChartsXAxisData -} - -func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ex.Data) -} - -type EChartsAxisLabel struct { - Formatter string `json:"formatter"` -} -type EChartsYAxisData struct { - Min *float64 `json:"min"` - Max *float64 `json:"max"` - AxisLabel EChartsAxisLabel `json:"axisLabel"` - AxisLine struct { - LineStyle struct { - Color string `json:"color"` - } `json:"lineStyle"` - } `json:"axisLine"` - Data []string `json:"data"` -} -type EChartsYAxis struct { - Data []EChartsYAxisData `json:"data"` -} - -func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ey.Data) -} - -type EChartsPadding struct { - Box chart.Box -} - -func (eb *EChartsPadding) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - arr := make([]int, 0) - err := json.Unmarshal(data, &arr) - if err != nil { - return err - } - if len(arr) == 0 { - return nil - } - switch len(arr) { - case 1: - eb.Box = chart.Box{ - Left: arr[0], - Top: arr[0], - Bottom: arr[0], - Right: arr[0], - } - case 2: - eb.Box = chart.Box{ - Top: arr[0], - Bottom: arr[0], - Left: arr[1], - Right: arr[1], - } - default: - result := make([]int, 4) - copy(result, arr) - if len(arr) == 3 { - result[3] = result[1] - } - // 上右下左 - eb.Box = chart.Box{ - Top: result[0], - Right: result[1], - Bottom: result[2], - Left: result[3], - } - } - return nil -} - -type EChartsLabelOption struct { - Show bool `json:"show"` - Distance int `json:"distance"` - Color string `json:"color"` -} -type EChartsLegend struct { - Show *bool `json:"show"` - Data []string `json:"data"` - Align string `json:"align"` - Orient string `json:"orient"` - Padding EChartsPadding `json:"padding"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` -} - -type EChartsMarkData struct { - Type string `json:"type"` -} -type _EChartsMarkData EChartsMarkData - -func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - data = convertToArray(data) - ds := make([]*_EChartsMarkData, 0) - err := json.Unmarshal(data, &ds) - if err != nil { - return err - } - for _, d := range ds { - if d.Type != "" { - emd.Type = d.Type - } - } - return nil -} - -type EChartsMarkPoint struct { - SymbolSize int `json:"symbolSize"` - Data []EChartsMarkData `json:"data"` -} - -func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint { - sp := SeriesMarkPoint{ - SymbolSize: emp.SymbolSize, - } - if len(emp.Data) == 0 { - return sp - } - data := make([]SeriesMarkData, len(emp.Data)) - for index, item := range emp.Data { - data[index].Type = item.Type - } - sp.Data = data - return sp -} - -type EChartsMarkLine struct { - Data []EChartsMarkData `json:"data"` -} - -func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine { - sl := SeriesMarkLine{} - if len(eml.Data) == 0 { - return sl - } - data := make([]SeriesMarkData, len(eml.Data)) - for index, item := range eml.Data { - data[index].Type = item.Type - } - sl.Data = data - return sl -} - -type EChartsSeries struct { - Data []EChartsSeriesData `json:"data"` - Name string `json:"name"` - Type string `json:"type"` - Radius string `json:"radius"` - YAxisIndex int `json:"yAxisIndex"` - ItemStyle EChartStyle `json:"itemStyle"` - // label的配置 - Label EChartsLabelOption `json:"label"` - MarkPoint EChartsMarkPoint `json:"markPoint"` - MarkLine EChartsMarkLine `json:"markLine"` - Max *float64 `json:"max"` - Min *float64 `json:"min"` -} -type EChartsSeriesList []EChartsSeries - -func (esList EChartsSeriesList) ToSeriesList() SeriesList { - seriesList := make(SeriesList, 0, len(esList)) - for _, item := range esList { - // 如果是pie,则每个子荐生成一个series - if item.Type == ChartTypePie { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Type: item.Type, - Name: dataItem.Name, - Label: SeriesLabel{ - Show: true, - }, - Radius: item.Radius, - Data: []SeriesData{ - { - Value: dataItem.Value.First(), - }, - }, - }) - } - continue - } - // 如果是radar或funnel - if item.Type == ChartTypeRadar || - item.Type == ChartTypeFunnel { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Name: dataItem.Name, - Type: item.Type, - Data: NewSeriesDataFromValues(dataItem.Value.values), - Max: item.Max, - Min: item.Min, - Label: SeriesLabel{ - Color: parseColor(item.Label.Color), - Show: item.Label.Show, - Distance: item.Label.Distance, - }, - }) - } - continue - } - data := make([]SeriesData, len(item.Data)) - for j, dataItem := range item.Data { - data[j] = SeriesData{ - Value: dataItem.Value.First(), - Style: dataItem.ItemStyle.ToStyle(), - } - } - seriesList = append(seriesList, Series{ - Type: item.Type, - Data: data, - AxisIndex: item.YAxisIndex, - Style: item.ItemStyle.ToStyle(), - Label: SeriesLabel{ - Color: parseColor(item.Label.Color), - Show: item.Label.Show, - Distance: item.Label.Distance, - }, - Name: item.Name, - MarkPoint: item.MarkPoint.ToSeriesMarkPoint(), - MarkLine: item.MarkLine.ToSeriesMarkLine(), - }) - } - return seriesList -} - -type EChartsTextStyle struct { - Color string `json:"color"` - FontFamily string `json:"fontFamily"` - FontSize float64 `json:"fontSize"` -} - -func (et *EChartsTextStyle) ToStyle() chart.Style { - s := chart.Style{ - FontSize: et.FontSize, - FontColor: parseColor(et.Color), - } - if et.FontFamily != "" { - s.Font, _ = GetFont(et.FontFamily) - } - return s -} - -type EChartsOption struct { - Type string `json:"type"` - Theme string `json:"theme"` - FontFamily string `json:"fontFamily"` - Padding EChartsPadding `json:"padding"` - Box chart.Box `json:"box"` - Width int `json:"width"` - Height int `json:"height"` - Title struct { - Text string `json:"text"` - Subtext string `json:"subtext"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` - SubtextStyle EChartsTextStyle `json:"subtextStyle"` - } `json:"title"` - XAxis EChartsXAxis `json:"xAxis"` - YAxis EChartsYAxis `json:"yAxis"` - Legend EChartsLegend `json:"legend"` - Radar struct { - Indicator []RadarIndicator `json:"indicator"` - } `json:"radar"` - Series EChartsSeriesList `json:"series"` - Children []EChartsOption `json:"children"` -} - -func (eo *EChartsOption) ToOption() ChartOption { - fontFamily := eo.FontFamily - if len(fontFamily) == 0 { - fontFamily = eo.Title.TextStyle.FontFamily - } - titleTextStyle := eo.Title.TextStyle.ToStyle() - titleSubtextStyle := eo.Title.SubtextStyle.ToStyle() - legendTextStyle := eo.Legend.TextStyle.ToStyle() - o := ChartOption{ - Type: eo.Type, - FontFamily: fontFamily, - Theme: eo.Theme, - Title: TitleOption{ - Text: eo.Title.Text, - Subtext: eo.Title.Subtext, - FontColor: titleTextStyle.FontColor, - FontSize: titleTextStyle.FontSize, - SubtextFontSize: titleSubtextStyle.FontSize, - SubtextFontColor: titleSubtextStyle.FontColor, - Left: string(eo.Title.Left), - Top: string(eo.Title.Top), - }, - Legend: LegendOption{ - Show: eo.Legend.Show, - FontSize: legendTextStyle.FontSize, - FontColor: legendTextStyle.FontColor, - Data: eo.Legend.Data, - Left: string(eo.Legend.Left), - Top: string(eo.Legend.Top), - Align: eo.Legend.Align, - Orient: eo.Legend.Orient, - }, - RadarIndicators: eo.Radar.Indicator, - Width: eo.Width, - Height: eo.Height, - Padding: eo.Padding.Box, - Box: eo.Box, - SeriesList: eo.Series.ToSeriesList(), - } - isHorizontalChart := false - for _, item := range eo.XAxis.Data { - if item.Type == "value" { - isHorizontalChart = true - } - } - if isHorizontalChart { - for index := range o.SeriesList { - series := o.SeriesList[index] - if series.Type == ChartTypeBar { - o.SeriesList[index].Type = ChartTypeHorizontalBar - } - } - } - - if len(eo.XAxis.Data) != 0 { - xAxisData := eo.XAxis.Data[0] - o.XAxis = XAxisOption{ - BoundaryGap: xAxisData.BoundaryGap, - Data: xAxisData.Data, - SplitNumber: xAxisData.SplitNumber, - } - } - yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data)) - for index, item := range eo.YAxis.Data { - yAxisOptions[index] = YAxisOption{ - Min: item.Min, - Max: item.Max, - Formatter: item.AxisLabel.Formatter, - Color: parseColor(item.AxisLine.LineStyle.Color), - Data: item.Data, - } - } - o.YAxisOptions = yAxisOptions - - if len(eo.Children) != 0 { - o.Children = make([]ChartOption, len(eo.Children)) - for index, item := range eo.Children { - o.Children[index] = item.ToOption() - } - } - return o -} - -func renderEcharts(options, outputType string) ([]byte, error) { - o := EChartsOption{} - err := json.Unmarshal([]byte(options), &o) - if err != nil { - return nil, err - } - opt := o.ToOption() - opt.Type = outputType - d, err := Render(opt) - if err != nil { - return nil, err - } - return d.Bytes() -} - -func RenderEChartsToPNG(options string) ([]byte, error) { - return renderEcharts(options, "png") -} - -func RenderEChartsToSVG(options string) ([]byte, error) { - return renderEcharts(options, "svg") -} diff --git a/echarts_test.go b/echarts_test.go deleted file mode 100644 index 2077278..0000000 --- a/echarts_test.go +++ /dev/null @@ -1,582 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestConvertToArray(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]byte(`[1]`), convertToArray([]byte("1"))) - assert.Equal([]byte(`[1]`), convertToArray([]byte("[1]"))) -} - -func TestEChartsPosition(t *testing.T) { - assert := assert.New(t) - var p EChartsPosition - err := p.UnmarshalJSON([]byte("1")) - assert.Nil(err) - assert.Equal(EChartsPosition("1"), p) - err = p.UnmarshalJSON([]byte(`"left"`)) - assert.Nil(err) - assert.Equal(EChartsPosition("left"), p) -} - -func TestEChartsSeriesDataValue(t *testing.T) { - assert := assert.New(t) - - es := EChartsSeriesDataValue{} - err := es.UnmarshalJSON([]byte(`[1, 2]`)) - assert.Nil(err) - assert.Equal(EChartsSeriesDataValue{ - values: []float64{ - 1, - 2, - }, - }, es) - assert.Equal(NewEChartsSeriesDataValue(1, 2), es) - assert.Equal(1.0, es.First()) -} - -func TestEChartsSeriesData(t *testing.T) { - assert := assert.New(t) - es := EChartsSeriesData{} - err := es.UnmarshalJSON([]byte("1.1")) - assert.Nil(err) - assert.Equal(EChartsSeriesDataValue{ - values: []float64{ - 1.1, - }, - }, es.Value) - - err = es.UnmarshalJSON([]byte(`{"value":200,"itemStyle":{"color":"#a90000"}}`)) - assert.Nil(err) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: EChartsSeriesDataValue{ - values: []float64{ - 200.0, - }, - }, - ItemStyle: EChartStyle{ - Color: "#a90000", - }, - }, es) -} - -func TestEChartsXAxis(t *testing.T) { - assert := assert.New(t) - ex := EChartsXAxis{} - err := ex.UnmarshalJSON([]byte(`{"boundaryGap": true, "splitNumber": 5, "data": ["a", "b"], "type": "value"}`)) - assert.Nil(err) - - assert.Equal(EChartsXAxis{ - Data: []EChartsXAxisData{ - { - BoundaryGap: TrueFlag(), - SplitNumber: 5, - Data: []string{ - "a", - "b", - }, - Type: "value", - }, - }, - }, ex) -} - -func TestEChartStyle(t *testing.T) { - assert := assert.New(t) - - es := EChartStyle{ - Color: "#999", - } - color := drawing.Color{ - R: 153, - G: 153, - B: 153, - A: 255, - } - assert.Equal(Style{ - FillColor: color, - FontColor: color, - StrokeColor: color, - }, es.ToStyle()) -} - -func TestEChartsPadding(t *testing.T) { - assert := assert.New(t) - - eb := EChartsPadding{} - - err := eb.UnmarshalJSON([]byte(`1`)) - assert.Nil(err) - assert.Equal(Box{ - Left: 1, - Top: 1, - Right: 1, - Bottom: 1, - }, eb.Box) - - err = eb.UnmarshalJSON([]byte(`[2, 3]`)) - assert.Nil(err) - assert.Equal(Box{ - Left: 3, - Top: 2, - Right: 3, - Bottom: 2, - }, eb.Box) - - err = eb.UnmarshalJSON([]byte(`[4, 5, 6]`)) - assert.Nil(err) - assert.Equal(Box{ - Left: 5, - Top: 4, - Right: 5, - Bottom: 6, - }, eb.Box) - - err = eb.UnmarshalJSON([]byte(`[4, 5, 6, 7]`)) - assert.Nil(err) - assert.Equal(Box{ - Left: 7, - Top: 4, - Right: 5, - Bottom: 6, - }, eb.Box) -} - -func TestEChartsMarkPoint(t *testing.T) { - assert := assert.New(t) - - emp := EChartsMarkPoint{ - SymbolSize: 30, - Data: []EChartsMarkData{ - { - Type: "test", - }, - }, - } - assert.Equal(SeriesMarkPoint{ - SymbolSize: 30, - Data: []SeriesMarkData{ - { - Type: "test", - }, - }, - }, emp.ToSeriesMarkPoint()) -} - -func TestEChartsMarkLine(t *testing.T) { - assert := assert.New(t) - - eml := EChartsMarkLine{ - Data: []EChartsMarkData{ - { - Type: "min", - }, - { - Type: "max", - }, - }, - } - assert.Equal(SeriesMarkLine{ - Data: []SeriesMarkData{ - { - Type: "min", - }, - { - Type: "max", - }, - }, - }, eml.ToSeriesMarkLine()) -} - -func TestEChartsOption(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - option string - }{ - { - option: `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - { - "value": 200, - "itemStyle": { - "color": "#a90000" - } - }, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - }, - { - option: `{ - "title": { - "text": "Referer of a Website", - "subtext": "Fake Data", - "left": "center" - }, - "tooltip": { - "trigger": "item" - }, - "legend": { - "orient": "vertical", - "left": "left" - }, - "series": [ - { - "name": "Access From", - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 1048, - "name": "Search Engine" - }, - { - "value": 735, - "name": "Direct" - }, - { - "value": 580, - "name": "Email" - }, - { - "value": 484, - "name": "Union Ads" - }, - { - "value": 300, - "name": "Video Ads" - } - ], - "emphasis": { - "itemStyle": { - "shadowBlur": 10, - "shadowOffsetX": 0, - "shadowColor": "rgba(0, 0, 0, 0.5)" - } - } - } - ] - }`, - }, - { - option: `{ - "title": { - "text": "Rainfall vs Evaporation", - "subtext": "Fake Data" - }, - "tooltip": { - "trigger": "axis" - }, - "legend": { - "data": [ - "Rainfall", - "Evaporation" - ] - }, - "toolbox": { - "show": true, - "feature": { - "dataView": { - "show": true, - "readOnly": false - }, - "magicType": { - "show": true, - "type": [ - "line", - "bar" - ] - }, - "restore": { - "show": true - }, - "saveAsImage": { - "show": true - } - } - }, - "calculable": true, - "xAxis": [ - { - "type": "category", - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "yAxis": [ - { - "type": "value" - } - ], - "series": [ - { - "name": "Rainfall", - "type": "bar", - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ], - "markPoint": { - "data": [ - { - "type": "max", - "name": "Max" - }, - { - "type": "min", - "name": "Min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average", - "name": "Avg" - } - ] - } - }, - { - "name": "Evaporation", - "type": "bar", - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ], - "markPoint": { - "data": [ - { - "name": "Max", - "value": 182.2, - "xAxis": 7, - "yAxis": 183 - }, - { - "name": "Min", - "value": 2.3, - "xAxis": 11, - "yAxis": 3 - } - ] - }, - "markLine": { - "data": [ - { - "type": "average", - "name": "Avg" - } - ] - } - } - ] - }`, - }, - } - for _, tt := range tests { - opt := EChartsOption{} - err := json.Unmarshal([]byte(tt.option), &opt) - assert.Nil(err) - assert.NotEmpty(opt.Series) - assert.NotEmpty(opt.ToOption().SeriesList) - } -} - -func TestRenderEChartsToSVG(t *testing.T) { - assert := assert.New(t) - - data, err := RenderEChartsToSVG(`{ - "title": { - "text": "Rainfall vs Evaporation", - "subtext": "Fake Data" - }, - "legend": { - "data": [ - "Rainfall", - "Evaporation" - ] - }, - "padding": [10, 30, 10, 10], - "xAxis": [ - { - "type": "category", - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "series": [ - { - "name": "Rainfall", - "type": "bar", - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Evaporation", - "type": "bar", - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - } - ] - }`) - assert.Nil(err) - assert.Equal("\\nRainfallEvaporationRainfall vs EvaporationFake Data24020016012080400FebMayAugNov162.22182.22.341.6248.07", string(data)) -} diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..ad8111b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +if [ "${1:0:1}" = '-' ]; then + set -- go-charts "$@" +fi + +exec "$@" \ No newline at end of file diff --git a/examples/area_line_chart/main.go b/examples/area_line_chart/main.go deleted file mode 100644 index 57ca1e9..0000000 --- a/examples/area_line_chart/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "area-line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - } - p, err := charts.LineRender( - values, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - charts.LegendLabelsOptionFunc([]string{ - "Email", - }, "50"), - func(opt *charts.ChartOption) { - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.FillArea = true - }, - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/bar_chart/main.go b/examples/bar_chart/main.go deleted file mode 100644 index 91c9f81..0000000 --- a/examples/bar_chart/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "bar-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }, - { - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }, - } - p, err := charts.BarRender( - values, - charts.XAxisDataOptionFunc([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - charts.LegendLabelsOptionFunc([]string{ - "Rainfall", - "Evaporation", - }, charts.PositionRight), - charts.MarkLineOptionFunc(0, charts.SeriesMarkDataTypeAverage), - charts.MarkPointOptionFunc(0, charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin), - // custom option func - func(opt *charts.ChartOption) { - opt.SeriesList[1].MarkPoint = charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ) - opt.SeriesList[1].MarkLine = charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ) - }, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/charts/main.go b/examples/charts/main.go deleted file mode 100644 index 81bc4f2..0000000 --- a/examples/charts/main.go +++ /dev/null @@ -1,1974 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "net/http" - "strconv" - - charts "git.smarteching.com/zeni/go-charts/v2" -) - -var html = ` - - - - - - - go-charts - - -
{{body}}
- - -` - -func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.ChartOption, echartsOptions []string) { - if req.URL.Path != "/" && - req.URL.Path != "/echarts" { - return - } - query := req.URL.Query() - theme := query.Get("theme") - width, _ := strconv.Atoi(query.Get("width")) - height, _ := strconv.Atoi(query.Get("height")) - charts.SetDefaultWidth(width) - charts.SetDefaultWidth(height) - bytesList := make([][]byte, 0) - for _, opt := range chartOptions { - opt.Theme = theme - opt.Type = charts.ChartOutputSVG - d, err := charts.Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - } - for _, opt := range echartsOptions { - buf, err := charts.RenderEChartsToSVG(opt) - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - } - - p, err := charts.TableOptionRender(charts.TableChartOption{ - Type: charts.ChartOutputSVG, - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }) - if err != nil { - panic(err) - } - buf, err := p.Bytes() - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - - data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) - w.Header().Set("Content-Type", "text/html") - w.Write(data) -} - -func indexHandler(w http.ResponseWriter, req *http.Request) { - chartOptions := []charts.ChartOption{ - { - Title: charts.TitleOption{ - Text: "Line", - }, - Legend: charts.NewLegendOption([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - charts.NewSeriesFromValues([]float64{ - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }), - charts.NewSeriesFromValues([]float64{ - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }), - charts.NewSeriesFromValues([]float64{ - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }), - charts.NewSeriesFromValues([]float64{ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }), - }, - }, - // 温度折线图 - { - Title: charts.TitleOption{ - Text: "Temperature Change in the Coming Week", - }, - Padding: charts.Box{ - Top: 20, - Left: 20, - Right: 30, - Bottom: 20, - }, - Legend: charts.NewLegendOption([]string{ - "Highest", - "Lowest", - }, charts.PositionRight), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, charts.FalseFlag()), - SeriesList: []charts.Series{ - { - Data: charts.NewSeriesDataFromValues([]float64{ - 14, - 11, - 13, - 11, - 12, - 12, - 7, - }), - MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 1, - -2, - 2, - 5, - 3, - 2, - 0, - }), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - }, - }, - { - Title: charts.TitleOption{ - Text: "Line Area", - }, - Legend: charts.NewLegendOption([]string{ - "Email", - }), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - }, - FillArea: true, - }, - // 柱状图 - { - Title: charts.TitleOption{ - Text: "Bar", - }, - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - Legend: charts.LegendOption{ - Data: []string{ - "Rainfall", - "Evaporation", - }, - Icon: charts.IconRect, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 200, - 150, - 80, - 70, - 110, - 130, - }, charts.ChartTypeBar), - { - Type: charts.ChartTypeBar, - Data: []charts.SeriesData{ - { - Value: 100, - }, - { - Value: 190, - Style: charts.Style{ - FillColor: charts.Color{ - R: 169, - G: 0, - B: 0, - A: 255, - }, - }, - }, - { - Value: 230, - }, - { - Value: 140, - }, - { - Value: 100, - }, - { - Value: 200, - }, - { - Value: 180, - }, - }, - Label: charts.SeriesLabel{ - Show: true, - Position: charts.PositionBottom, - }, - }, - }, - }, - // 水平柱状图 - { - Title: charts.TitleOption{ - Text: "World Population", - }, - Padding: charts.Box{ - Top: 20, - Right: 40, - Bottom: 20, - Left: 20, - }, - Legend: charts.NewLegendOption([]string{ - "2011", - "2012", - }), - YAxisOptions: charts.NewYAxisOptions([]string{ - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World", - }), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeHorizontalBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, - }), - }, - { - Type: charts.ChartTypeHorizontalBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, - }), - }, - }, - }, - // 柱状图+标记 - { - Title: charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - }, - Padding: charts.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Rainfall", - "Evaporation", - }, charts.PositionRight), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - }, - }, - // 双Y轴示例 - { - Title: charts.TitleOption{ - Text: "Temperature", - }, - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Evaporation", - "Precipitation", - "Temperature", - }), - YAxisOptions: []charts.YAxisOption{ - { - Formatter: "{value}ml", - Color: charts.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }, - { - Formatter: "{value}°C", - Color: charts.Color{ - R: 250, - G: 200, - B: 88, - A: 255, - }, - }, - }, - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 4.9, - 7.0, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20.0, - 6.4, - 3.3, - }), - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }), - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23.0, - 16.5, - 12.0, - 6.2, - }), - AxisIndex: 1, - }, - }, - }, - // 饼图 - { - Title: charts.TitleOption{ - Text: "Referer of a Website", - Subtext: "Fake Data", - Left: charts.PositionCenter, - }, - Legend: charts.LegendOption{ - Orient: charts.OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: charts.PositionLeft, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 1048, - 735, - 580, - 484, - 300, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - // 雷达图 - { - Title: charts.TitleOption{ - Text: "Basic Radar Chart", - }, - Legend: charts.NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - RadarIndicators: []charts.RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: charts.SeriesList{ - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, - // 漏斗图 - { - Title: charts.TitleOption{ - Text: "Funnel", - }, - Legend: charts.NewLegendOption([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeFunnel, - Name: "Show", - Data: charts.NewSeriesDataFromValues([]float64{ - 100, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Click", - Data: charts.NewSeriesDataFromValues([]float64{ - 80, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Visit", - Data: charts.NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Inquiry", - Data: charts.NewSeriesDataFromValues([]float64{ - 40, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Order", - Data: charts.NewSeriesDataFromValues([]float64{ - 20, - }), - }, - }, - }, - // 多图展示 - { - Legend: charts.LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: charts.Box{ - Top: 100, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: charts.NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisOptions: []charts.YAxisOption{ - { - - Min: charts.NewFloatPoint(0), - Max: charts.NewFloatPoint(90), - }, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - charts.NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - charts.NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, charts.ChartTypeBar), - charts.NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, charts.ChartTypeBar), - }, - Children: []charts.ChartOption{ - { - Legend: charts.LegendOption{ - Show: charts.FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: charts.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - }, - } - handler(w, req, chartOptions, nil) -} - -func echartsHandler(w http.ResponseWriter, req *http.Request) { - echartsOptions := []string{ - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 150, - 230, - 224, - 218, - 135, - 147, - 260 - ], - "type": "line" - } - ] - }`, - `{ - "title": { - "text": "Multiple Line" - }, - "tooltip": { - "trigger": "axis" - }, - "legend": { - "left": "right", - "data": [ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine" - ] - }, - "grid": { - "left": "3%", - "right": "4%", - "bottom": "3%", - "containLabel": true - }, - "toolbox": { - "feature": { - "saveAsImage": {} - } - }, - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "name": "Email", - "type": "line", - "data": [ - 120, - 132, - 101, - 134, - 90, - 230, - 210 - ] - }, - { - "name": "Union Ads", - "type": "line", - "data": [ - 220, - 182, - 191, - 234, - 290, - 330, - 310 - ] - }, - { - "name": "Video Ads", - "type": "line", - "data": [ - 150, - 232, - 201, - 154, - 190, - 330, - 410 - ] - }, - { - "name": "Direct", - "type": "line", - "data": [ - 320, - 332, - 301, - 334, - 390, - 330, - 320 - ] - }, - { - "name": "Search Engine", - "type": "line", - "data": [ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320 - ] - } - ] - }`, - `{ - "title": { - "text": "Temperature Change in the Coming Week" - }, - "legend": { - "left": "right" - }, - "padding": [10, 30, 10, 10], - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} °C" - } - }, - "series": [ - { - "name": "Highest", - "type": "line", - "data": [ - 10, - 11, - 13, - 11, - 12, - 12, - 9 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Lowest", - "type": "line", - "data": [ - 1, - -2, - 2, - 5, - 3, - 2, - 0 - ], - "markPoint": { - "data": [ - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - }, - { - "type": "max" - } - ] - } - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - 200, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - { - "value": 200, - "itemStyle": { - "color": "#a90000" - } - }, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "title": { - "text": "World Population" - }, - "tooltip": { - "trigger": "axis", - "axisPointer": { - "type": "shadow" - } - }, - "legend": {}, - "grid": { - "left": "3%", - "right": "4%", - "bottom": "3%", - "containLabel": true - }, - "xAxis": { - "type": "value" - }, - "yAxis": { - "type": "category", - "data": [ - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World" - ] - }, - "series": [ - { - "name": "2011", - "type": "bar", - "data": [ - 18203, - 23489, - 29034, - 104970, - 131744, - 630230 - ] - }, - { - "name": "2012", - "type": "bar", - "data": [ - 19325, - 23438, - 31000, - 121594, - 134141, - 681807 - ] - } - ] - }`, - `{ - "title": { - "text": "Rainfall vs Evaporation", - "subtext": "Fake Data" - }, - "legend": { - "data": [ - "Rainfall", - "Evaporation" - ] - }, - "padding": [10, 30, 10, 10], - "xAxis": [ - { - "type": "category", - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "series": [ - { - "name": "Rainfall", - "type": "bar", - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Evaporation", - "type": "bar", - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - } - ] - }`, - `{ - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ], - "axisPointer": { - "type": "shadow" - } - } - ], - "yAxis": [ - { - "type": "value", - "name": "Precipitation", - "min": 0, - "max": 240, - "axisLabel": { - "formatter": "{value} ml" - } - }, - { - "type": "value", - "name": "Temperature", - "min": 0, - "max": 24, - "axisLabel": { - "formatter": "{value} °C" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "tooltip": {}, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "tooltip": {}, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6 - ] - }, - { - "name": "Temperature", - "type": "line", - "yAxisIndex": 1, - "tooltip": {}, - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3 - ] - } - ] - }`, - `{ - "tooltip": { - "trigger": "axis", - "axisPointer": { - "type": "cross" - } - }, - "grid": { - "right": "20%" - }, - "toolbox": { - "feature": { - "dataView": { - "show": true, - "readOnly": false - }, - "restore": { - "show": true - }, - "saveAsImage": { - "show": true - } - } - }, - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "axisTick": { - "alignWithLabel": true - }, - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "yAxis": [ - { - "type": "value", - "name": "温度", - "position": "left", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#EE6666" - } - }, - "axisLabel": { - "formatter": "{value} °C" - } - }, - { - "type": "value", - "name": "Evaporation", - "position": "right", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#5470C6" - } - }, - "axisLabel": { - "formatter": "{value} ml" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ] - }, - { - "name": "Temperature", - "type": "line", - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 - ] - } - ] - }`, - `{ - "title": { - "text": "Referer of a Website", - "subtext": "Fake Data", - "left": "center" - }, - "tooltip": { - "trigger": "item" - }, - "legend": { - "orient": "vertical", - "left": "left" - }, - "series": [ - { - "name": "Access From", - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 1048, - "name": "Search Engine" - }, - { - "value": 735, - "name": "Direct" - }, - { - "value": 580, - "name": "Email" - }, - { - "value": 484, - "name": "Union Ads" - }, - { - "value": 300, - "name": "Video Ads" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Rainfall" - }, - "padding": [10, 10, 10, 30], - "legend": { - "data": [ - "GZ", - "SH" - ] - }, - "xAxis": { - "type": "category", - "splitNumber": 6, - "data": [ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - "01-10", - "01-11", - "01-12", - "01-13", - "01-14", - "01-15", - "01-16", - "01-17", - "01-18", - "01-19", - "01-20", - "01-21", - "01-22", - "01-23", - "01-24", - "01-25", - "01-26", - "01-27", - "01-28", - "01-29", - "01-30", - "01-31" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} mm" - } - }, - "series": [ - { - "type": "bar", - "data": [ - 928, - 821, - 889, - 600, - 547, - 783, - 197, - 853, - 430, - 346, - 63, - 465, - 309, - 334, - 141, - 538, - 792, - 58, - 922, - 807, - 298, - 243, - 744, - 885, - 812, - 231, - 330, - 220, - 984, - 221, - 429 - ] - }, - { - "type": "bar", - "data": [ - 749, - 201, - 296, - 579, - 255, - 159, - 902, - 246, - 149, - 158, - 507, - 776, - 186, - 79, - 390, - 222, - 601, - 367, - 221, - 411, - 714, - 620, - 966, - 73, - 203, - 631, - 833, - 610, - 487, - 677, - 596 - ] - } - ] - }`, - `{ - "title": { - "text": "Basic Radar Chart" - }, - "legend": { - "data": [ - "Allocated Budget", - "Actual Spending" - ] - }, - "radar": { - "indicator": [ - { - "name": "Sales", - "max": 6500 - }, - { - "name": "Administration", - "max": 16000 - }, - { - "name": "Information Technology", - "max": 30000 - }, - { - "name": "Customer Support", - "max": 38000 - }, - { - "name": "Development", - "max": 52000 - }, - { - "name": "Marketing", - "max": 25000 - } - ] - }, - "series": [ - { - "name": "Budget vs spending", - "type": "radar", - "data": [ - { - "value": [ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000 - ], - "name": "Allocated Budget" - }, - { - "value": [ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000 - ], - "name": "Actual Spending" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Funnel" - }, - "tooltip": { - "trigger": "item", - "formatter": "{a}
{b} : {c}%" - }, - "toolbox": { - "feature": { - "dataView": { - "readOnly": false - }, - "restore": {}, - "saveAsImage": {} - } - }, - "legend": { - "data": [ - "Show", - "Click", - "Visit", - "Inquiry", - "Order" - ] - }, - "series": [ - { - "name": "Funnel", - "type": "funnel", - "left": "10%", - "top": 60, - "bottom": 60, - "width": "80%", - "min": 0, - "max": 100, - "minSize": "0%", - "maxSize": "100%", - "sort": "descending", - "gap": 2, - "label": { - "show": true, - "position": "inside" - }, - "labelLine": { - "length": 10, - "lineStyle": { - "width": 1, - "type": "solid" - } - }, - "itemStyle": { - "borderColor": "#fff", - "borderWidth": 1 - }, - "emphasis": { - "label": { - "fontSize": 20 - } - }, - "data": [ - { - "value": 60, - "name": "Visit" - }, - { - "value": 40, - "name": "Inquiry" - }, - { - "value": 20, - "name": "Order" - }, - { - "value": 80, - "name": "Click" - }, - { - "value": 100, - "name": "Show" - } - ] - } - ] - }`, - `{ - "legend": { - "top": "-140", - "data": [ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie" - ] - }, - "padding": [ - 150, - 10, - 10, - 10 - ], - "xAxis": [ - { - "data": [ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017" - ] - } - ], - "series": [ - { - "data": [ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1 - ] - }, - { - "data": [ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7 - ] - }, - { - "data": [ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5 - ] - }, - { - "data": [ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1 - ] - } - ], - "children": [ - { - "box": { - "left": 0, - "top": 30, - "right": 600, - "bottom": 150 - }, - "legend": { - "show": false - }, - "series": [ - { - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 435.9, - "name": "Milk Tea" - }, - { - "value": 354.3, - "name": "Matcha Latte" - }, - { - "value": 285.9, - "name": "Cheese Cocoa" - }, - { - "value": 204.5, - "name": "Walnut Brownie" - } - ] - } - ] - } - ] - }`, - } - handler(w, req, nil, echartsOptions) -} - -func main() { - http.HandleFunc("/", indexHandler) - http.HandleFunc("/echarts", echartsHandler) - fmt.Println("http://127.0.0.1:3012/") - http.ListenAndServe(":3012", nil) -} diff --git a/examples/chinese/main.go b/examples/chinese/main.go deleted file mode 100644 index 601f54e..0000000 --- a/examples/chinese/main.go +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "chinese-line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - // 字体文件需要自行下载 - // https://github.com/googlefonts/noto-cjk - buf, err := ioutil.ReadFile("./NotoSansSC.ttf") - if err != nil { - panic(err) - } - err = charts.InstallFont("noto", buf) - if err != nil { - panic(err) - } - font, _ := charts.GetFont("noto") - charts.SetDefaultFont(font) - - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - { - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }, - { - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }, - { - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }, - { - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }, - } - p, err := charts.LineRender( - values, - charts.TitleTextOptionFunc("测试"), - charts.XAxisDataOptionFunc([]string{ - "星期一", - "星期二", - "星期三", - "星期四", - "星期五", - "星期六", - "星期日", - }), - charts.LegendLabelsOptionFunc([]string{ - "邮件", - "广告", - "视频广告", - "直接访问", - "搜索引擎", - }, charts.PositionCenter), - ) - - if err != nil { - panic(err) - } - - buf, err = p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/funnel_chart/main.go b/examples/funnel_chart/main.go deleted file mode 100644 index 653f834..0000000 --- a/examples/funnel_chart/main.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "funnel-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := []float64{ - 100, - 80, - 60, - 40, - 20, - 10, - 0, - } - p, err := charts.FunnelRender( - values, - charts.TitleTextOptionFunc("Funnel"), - charts.LegendLabelsOptionFunc([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - "Pay", - "Cancel", - }), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go deleted file mode 100644 index f5d8497..0000000 --- a/examples/horizontal_bar_chart/main.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "horizontal-bar-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 10, - 30, - 50, - 70, - 90, - 110, - 130, - }, - { - 20, - 40, - 60, - 80, - 100, - 120, - 140, - }, - } - p, err := charts.HorizontalBarRender( - values, - charts.TitleTextOptionFunc("World Population"), - charts.PaddingOptionFunc(charts.Box{ - Top: 20, - Right: 40, - Bottom: 20, - Left: 20, - }), - charts.LegendLabelsOptionFunc([]string{ - "2011", - "2012", - }), - charts.YAxisDataOptionFunc([]string{ - "UN", - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World", - }), - func(opt *charts.ChartOption) { - opt.SeriesList[0].RoundRadius = 5 - }, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/line_chart/main.go b/examples/line_chart/main.go deleted file mode 100644 index baee8a3..0000000 --- a/examples/line_chart/main.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 120, - 132, - 101, - // 134, - charts.GetNullValue(), - 90, - 230, - 210, - }, - { - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }, - { - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }, - { - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }, - { - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }, - } - p, err := charts.LineRender( - values, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, "50"), - func(opt *charts.ChartOption) { - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.YAxisOptions = []charts.YAxisOption{ - { - SplitLineShow: charts.FalseFlag(), - }, - } - opt.SymbolShow = charts.FalseFlag() - opt.LineStrokeWidth = 1 - opt.ValueFormatter = func(f float64) string { - return fmt.Sprintf("%.0f", f) - } - }, - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/painter/main.go b/examples/painter/main.go deleted file mode 100644 index 1b842b3..0000000 --- a/examples/painter/main.go +++ /dev/null @@ -1,607 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - charts "git.smarteching.com/zeni/go-charts/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "painter.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - p, err := charts.NewPainter(charts.PainterOptions{ - Width: 600, - Height: 2000, - Type: charts.ChartOutputPNG, - }) - if err != nil { - panic(err) - } - // 背景色 - p.SetBackground(p.Width(), p.Height(), drawing.ColorWhite) - - top := 0 - - // 画线 - p.SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - }) - p.LineStroke([]charts.Point{ - { - X: 0, - Y: 0, - }, - { - X: 100, - Y: 10, - }, - { - X: 200, - Y: 0, - }, - { - X: 300, - Y: 10, - }, - }) - - // 圆滑曲线 - // top += 50 - // p.Child(charts.PainterPaddingOption(charts.Box{ - // Top: top, - // })).SetDrawingStyle(charts.Style{ - // StrokeColor: drawing.ColorBlack, - // FillColor: drawing.ColorBlack, - // StrokeWidth: 1, - // }).SmoothLineStroke([]charts.Point{ - // { - // X: 0, - // Y: 0, - // }, - // { - // X: 100, - // Y: 10, - // }, - // { - // X: 200, - // Y: 0, - // }, - // { - // X: 300, - // Y: 10, - // }, - // }) - - // 标线 - top += 50 - p.Child(charts.PainterPaddingOption(charts.Box{ - Top: top, - })).SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - StrokeDashArray: []float64{ - 4, - 2, - }, - }).MarkLine(0, 0, p.Width()) - - top += 60 - // Polygon - p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - })).SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - }).Polygon(charts.Point{ - X: 100, - Y: 0, - }, 50, 6) - - // FillArea - top += 60 - p.Child(charts.PainterPaddingOption(charts.Box{ - Top: top, - })).SetDrawingStyle(charts.Style{ - FillColor: drawing.ColorBlack, - }).FillArea([]charts.Point{ - { - X: 0, - Y: 0, - }, - { - X: 100, - Y: 0, - }, - { - X: 150, - Y: 40, - }, - { - X: 80, - Y: 30, - }, - { - X: 0, - Y: 0, - }, - }) - - // 坐标轴的点 - top += 50 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: 20, - }), - ).SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - }).Ticks(charts.TicksOption{ - Count: 7, - Length: 5, - }) - - // 坐标轴的点,每2格显示一个 - top += 20 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: 20, - }), - ).SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - }).Ticks(charts.TicksOption{ - Unit: 2, - Count: 7, - Length: 5, - }) - - // 坐标轴的点,纵向 - top += 20 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 100, - }), - ).SetDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - StrokeWidth: 1, - }).Ticks(charts.TicksOption{ - Orient: charts.OrientVertical, - Count: 7, - Length: 5, - }) - - // 横向展示文本 - top += 120 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: 20, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).MultiText(charts.MultiTextOption{ - TextList: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }) - - // 横向显示文本,靠左 - top += 20 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: 20, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).MultiText(charts.MultiTextOption{ - Position: charts.PositionLeft, - TextList: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }) - - // 纵向显示文本 - top += 20 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: 50, - Bottom: top + 150, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).MultiText(charts.MultiTextOption{ - Orient: charts.OrientVertical, - Align: charts.AlignRight, - TextList: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }) - // 纵向 文本居中 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 50, - Right: 100, - Bottom: top + 150, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).MultiText(charts.MultiTextOption{ - Orient: charts.OrientVertical, - Align: charts.AlignCenter, - TextList: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }) - // 纵向 文本置顶 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 100, - Right: 150, - Bottom: top + 150, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).MultiText(charts.MultiTextOption{ - Orient: charts.OrientVertical, - Position: charts.PositionTop, - Align: charts.AlignCenter, - TextList: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }) - - // grid - top += 150 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 100, - }), - ).OverrideTextStyle(charts.Style{ - FontColor: drawing.ColorBlack, - FontSize: 10, - }).Grid(charts.GridOption{ - Column: 8, - IgnoreColumnLines: []int{0, 8}, - Row: 8, - IgnoreRowLines: []int{0, 8}, - }) - - // dots - top += 100 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 20, - }), - ).OverrideDrawingStyle(charts.Style{ - FillColor: drawing.ColorWhite, - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - }).Dots([]charts.Point{ - { - X: 0, - Y: 0, - }, - { - X: 50, - Y: 0, - }, - { - X: 100, - Y: 10, - }, - }) - - // rect - top += 30 - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: 200, - Bottom: top + 50, - }), - ).OverrideDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - }).Rect(charts.Box{ - Left: 10, - Top: 0, - Right: 110, - Bottom: 20, - }) - // legend line dot - p.Child( - charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 200, - Right: p.Width() - 1, - Bottom: top + 50, - }), - ).OverrideDrawingStyle(charts.Style{ - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlack, - }).LegendLineDot(charts.Box{ - Left: 10, - Top: 0, - Right: 50, - Bottom: 20, - }) - - // grid - top += 50 - charts.NewGridPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 100, - })), charts.GridPainterOption{ - Row: 5, - IgnoreFirstRow: true, - IgnoreLastRow: true, - StrokeColor: drawing.ColorBlue, - }).Render() - - // legend - top += 100 - charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 30, - })), charts.LegendOption{ - Left: "10", - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - }, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - - // legend - top += 30 - charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 30, - })), charts.LegendOption{ - Left: charts.PositionRight, - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - }, - Align: charts.AlignRight, - FontSize: 16, - Icon: charts.IconRect, - FontColor: drawing.ColorBlack, - }).Render() - - // legend - top += 30 - charts.NewLegendPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 100, - })), charts.LegendOption{ - Top: "10", - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - }, - Orient: charts.OrientVertical, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - - // axis bottom - top += 100 - charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 50, - })), charts.AxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - StrokeColor: drawing.ColorBlack, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - - // axis top - top += 50 - charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 1, - Right: p.Width() - 1, - Bottom: top + 50, - })), charts.AxisOption{ - Position: charts.PositionTop, - BoundaryGap: charts.FalseFlag(), - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - StrokeColor: drawing.ColorBlack, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - - // axis left - top += 50 - charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 10, - Right: 60, - Bottom: top + 200, - })), charts.AxisOption{ - Position: charts.PositionLeft, - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - StrokeColor: drawing.ColorBlack, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - // axis right - charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 100, - Right: 150, - Bottom: top + 200, - })), charts.AxisOption{ - Position: charts.PositionRight, - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - StrokeColor: drawing.ColorBlack, - FontSize: 12, - FontColor: drawing.ColorBlack, - }).Render() - - // axis left no tick - charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{ - Top: top, - Left: 150, - Right: 300, - Bottom: top + 200, - })), charts.AxisOption{ - BoundaryGap: charts.FalseFlag(), - Position: charts.PositionLeft, - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - FontSize: 12, - FontColor: drawing.ColorBlack, - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack.WithAlpha(100), - }).Render() - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go deleted file mode 100644 index 5d70438..0000000 --- a/examples/pie_chart/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "pie-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := []float64{ - 1048, - 735, - 580, - 484, - 300, - } - p, err := charts.PieRender( - values, - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - Left: charts.PositionCenter, - }), - charts.PaddingOptionFunc(charts.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }), - charts.LegendOptionFunc(charts.LegendOption{ - Orient: charts.OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: charts.PositionLeft, - }), - charts.PieSeriesShowLabel(), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/radar_chart/main.go b/examples/radar_chart/main.go deleted file mode 100644 index e7053af..0000000 --- a/examples/radar_chart/main.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "radar-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - values := [][]float64{ - { - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }, - { - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }, - } - p, err := charts.RadarRender( - values, - charts.TitleTextOptionFunc("Basic Radar Chart"), - charts.LegendLabelsOptionFunc([]string{ - "Allocated Budget", - "Actual Spending", - }), - charts.RadarIndicatorOptionFunc([]string{ - "Sales", - "Administration", - "Information Technology", - "Customer Support", - "Development", - "Marketing", - }, []float64{ - 6500, - 16000, - 30000, - 38000, - 52000, - 25000, - }), - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/examples/table/main.go b/examples/table/main.go deleted file mode 100644 index de994eb..0000000 --- a/examples/table/main.go +++ /dev/null @@ -1,178 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "strconv" - "strings" - - "git.smarteching.com/zeni/go-charts/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func writeFile(buf []byte, filename string) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, filename) - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - // charts.SetDefaultTableSetting(charts.TableDarkThemeSetting) - charts.SetDefaultWidth(810) - header := []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - } - data := [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - } - spans := map[int]int{ - 0: 2, - 1: 1, - // 设置第三列的span - 2: 3, - 3: 2, - 4: 2, - } - p, err := charts.TableRender( - header, - data, - spans, - ) - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf, "table.png") - if err != nil { - panic(err) - } - - bgColor := charts.Color{ - R: 16, - G: 22, - B: 30, - A: 255, - } - p, err = charts.TableOptionRender(charts.TableChartOption{ - Header: []string{ - "Name", - "Price", - "Change", - }, - BackgroundColor: bgColor, - HeaderBackgroundColor: bgColor, - RowBackgroundColors: []charts.Color{ - bgColor, - }, - HeaderFontColor: drawing.ColorWhite, - FontColor: drawing.ColorWhite, - Padding: charts.Box{ - Top: 15, - Right: 10, - Bottom: 15, - Left: 10, - }, - Data: [][]string{ - { - "Datadog Inc", - "97.32", - "-7.49%", - }, - { - "Hashicorp Inc", - "28.66", - "-9.25%", - }, - { - "Gitlab Inc", - "51.63", - "+4.32%", - }, - }, - TextAligns: []string{ - "", - charts.AlignRight, - charts.AlignRight, - }, - CellStyle: func(tc charts.TableCell) *charts.Style { - column := tc.Column - if column != 2 { - return nil - } - value, _ := strconv.ParseFloat(strings.Replace(tc.Text, "%", "", 1), 64) - if value == 0 { - return nil - } - style := charts.Style{ - Padding: charts.Box{ - Bottom: 5, - }, - } - if value > 0 { - style.FillColor = charts.Color{ - R: 179, - G: 53, - B: 20, - A: 255, - } - } else if value < 0 { - style.FillColor = charts.Color{ - R: 33, - G: 124, - B: 50, - A: 255, - } - } - return &style - }, - }) - if err != nil { - panic(err) - } - - buf, err = p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf, "table-color.png") - if err != nil { - panic(err) - } -} diff --git a/examples/time_line_chart/main.go b/examples/time_line_chart/main.go deleted file mode 100644 index c6c93bf..0000000 --- a/examples/time_line_chart/main.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "crypto/rand" - "fmt" - "math/big" - "os" - "path/filepath" - "time" - - "git.smarteching.com/zeni/go-charts/v2" -) - -func writeFile(buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file := filepath.Join(tmpPath, "time-line-chart.png") - err = os.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func main() { - xAxisValue := []string{} - values := []float64{} - now := time.Now() - firstAxis := 0 - for i := 0; i < 300; i++ { - // 设置首个axis为xx:00的时间点 - if firstAxis == 0 && now.Minute() == 0 { - firstAxis = i - } - xAxisValue = append(xAxisValue, now.Format("15:04")) - now = now.Add(time.Minute) - value, _ := rand.Int(rand.Reader, big.NewInt(100)) - values = append(values, float64(value.Int64())) - } - p, err := charts.LineRender( - [][]float64{ - values, - }, - charts.TitleTextOptionFunc("Line"), - charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()), - charts.LegendLabelsOptionFunc([]string{ - "Demo", - }, "50"), - func(opt *charts.ChartOption) { - opt.XAxis.FirstAxis = firstAxis - // 必须要比计算得来的最小值更大(每60分钟) - opt.XAxis.SplitNumber = 60 - opt.Legend.Padding = charts.Box{ - Top: 5, - Bottom: 10, - } - opt.SymbolShow = charts.FalseFlag() - opt.LineStrokeWidth = 1 - opt.ValueFormatter = func(f float64) string { - return fmt.Sprintf("%.0f", f) - } - }, - ) - - if err != nil { - panic(err) - } - - buf, err := p.Bytes() - if err != nil { - panic(err) - } - err = writeFile(buf) - if err != nil { - panic(err) - } -} diff --git a/font.go b/font.go deleted file mode 100644 index 828654e..0000000 --- a/font.go +++ /dev/null @@ -1,78 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "sync" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/roboto" -) - -var fonts = sync.Map{} -var ErrFontNotExists = errors.New("font is not exists") -var defaultFontFamily = "defaultFontFamily" - -func init() { - name := "roboto" - _ = InstallFont(name, roboto.Roboto) - font, _ := GetFont(name) - SetDefaultFont(font) -} - -// InstallFont installs the font for charts -func InstallFont(fontFamily string, data []byte) error { - font, err := truetype.Parse(data) - if err != nil { - return err - } - fonts.Store(fontFamily, font) - return nil -} - -// GetDefaultFont get default font -func GetDefaultFont() (*truetype.Font, error) { - return GetFont(defaultFontFamily) -} - -// SetDefaultFont set default font -func SetDefaultFont(font *truetype.Font) { - if font == nil { - return - } - fonts.Store(defaultFontFamily, font) -} - -// GetFont get the font by font family -func GetFont(fontFamily string) (*truetype.Font, error) { - value, ok := fonts.Load(fontFamily) - if !ok { - return nil, ErrFontNotExists - } - f, ok := value.(*truetype.Font) - if !ok { - return nil, ErrFontNotExists - } - return f, nil -} diff --git a/font_test.go b/font_test.go deleted file mode 100644 index e0c56b2..0000000 --- a/font_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/roboto" -) - -func TestInstallFont(t *testing.T) { - assert := assert.New(t) - - fontFamily := "test" - err := InstallFont(fontFamily, roboto.Roboto) - assert.Nil(err) - - font, err := GetFont(fontFamily) - assert.Nil(err) - assert.NotNil(font) -} diff --git a/funnel_chart.go b/funnel_chart.go deleted file mode 100644 index d4a8bdd..0000000 --- a/funnel_chart.go +++ /dev/null @@ -1,192 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" -) - -type funnelChart struct { - p *Painter - opt *FunnelChartOption -} - -// NewFunnelSeriesList returns a series list for funnel -func NewFunnelSeriesList(values []float64) SeriesList { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues([]float64{ - value, - }, ChartTypeFunnel) - } - return seriesList -} - -// NewFunnelChart returns a funnel chart renderer -func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &funnelChart{ - p: p, - opt: &opt, - } -} - -type FunnelChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The padding of line chart - Padding Box - // The option of title - Title TitleOption - // The legend option - Legend LegendOption -} - -func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - opt := f.opt - seriesPainter := result.seriesPainter - max := seriesList[0].Data[0].Value - min := float64(0) - for _, item := range seriesList { - if item.Max != nil { - max = *item.Max - } - if item.Min != nil { - min = *item.Min - } - } - theme := opt.Theme - gap := 2 - height := seriesPainter.Height() - width := seriesPainter.Width() - count := len(seriesList) - - h := (height - gap*(count-1)) / count - - y := 0 - widthList := make([]int, len(seriesList)) - textList := make([]string, len(seriesList)) - seriesNames := seriesList.Names() - offset := max - min - for index, item := range seriesList { - value := item.Data[0].Value - // 最大最小值一致则为100% - widthPercent := 100.0 - if offset != 0 { - widthPercent = (value - min) / offset - } - w := int(widthPercent * float64(width)) - widthList[index] = w - // 如果最大值为0,则占比100% - percent := 1.0 - if max != 0 { - percent = value / max - } - textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent) - } - - for index, w := range widthList { - series := seriesList[index] - nextWidth := 0 - if index+1 < len(widthList) { - nextWidth = widthList[index+1] - } - topStartX := (width - w) >> 1 - topEndX := topStartX + w - bottomStartX := (width - nextWidth) >> 1 - bottomEndX := bottomStartX + nextWidth - points := []Point{ - { - X: topStartX, - Y: y, - }, - { - X: topEndX, - Y: y, - }, - { - X: bottomEndX, - Y: y + h, - }, - { - X: bottomStartX, - Y: y + h, - }, - { - X: topStartX, - Y: y, - }, - } - color := theme.GetSeriesColor(series.index) - - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: color, - }).FillArea(points) - - // 文本 - text := textList[index] - seriesPainter.OverrideTextStyle(Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - }) - textBox := seriesPainter.MeasureText(text) - textX := width>>1 - textBox.Width()>>1 - textY := y + h>>1 - seriesPainter.Text(text, textX, textY) - y += (h + gap) - } - - return f.p.box, nil -} - -func (f *funnelChart) Render() (Box, error) { - p := f.p - opt := f.opt - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: XAxisOption{ - Show: FalseFlag(), - }, - YAxisOptions: []YAxisOption{ - { - Show: FalseFlag(), - }, - }, - TitleOption: opt.Title, - LegendOption: opt.Legend, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypeFunnel) - return f.render(renderResult, seriesList) -} diff --git a/funnel_chart_test.go b/funnel_chart_test.go deleted file mode 100644 index d260bfb..0000000 --- a/funnel_chart_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFunnelChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewFunnelChart(p, FunnelChartOption{ - SeriesList: NewFunnelSeriesList([]float64{ - 100, - 80, - 60, - 40, - 20, - }), - Legend: NewLegendOption([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - Title: TitleOption{ - Text: "Funnel", - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nShowClickVisitInquiryOrderFunnel(100%)(80%)(60%)(40%)(20%)", - }, - } - - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/go.mod b/go.mod index 76a47b6..60f8934 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,22 @@ -module git.smarteching.com/zeni/go-charts/v2 +module github.com/vicanso/go-charts-web -go 1.24.1 +go 1.19 require ( - git.smarteching.com/zeni/go-chart/v2 v2.1.4 - github.com/dustin/go-humanize v1.0.1 - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/stretchr/testify v1.10.0 + github.com/vicanso/elton v1.10.0 + github.com/vicanso/go-charts/v2 v2.6.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/image v0.21.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/vicanso/hes v0.6.1 // indirect + github.com/vicanso/intranet-ip v0.1.0 // indirect + github.com/vicanso/keygrip v1.2.1 // indirect + github.com/wcharczuk/go-chart/v2 v2.1.0 // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect ) diff --git a/go.sum b/go.sum index 3e1a48a..fba9022 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ -git.smarteching.com/zeni/go-chart/v2 v2.1.4 h1:pF06+F6eqJLIG8uMiTVPR5TygPGMjM/FHMzTxmu5V/Q= -git.smarteching.com/zeni/go-chart/v2 v2.1.4/go.mod h1:b3ueW9h3pGGXyhkormZAvilHaG4+mQti+bMNPdQBeOQ= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -8,11 +9,37 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/vicanso/elton v1.10.0 h1:Qd6Dr5sarzkij+Vdgvtsd08jaNAKuZpBDpyPtKzESaY= +github.com/vicanso/elton v1.10.0/go.mod h1:GnFxH3+Vtz0HQbhGhCmssbdi67+yLjFOUbZDdB8mRcQ= +github.com/vicanso/go-charts/v2 v2.6.0 h1:nDaJuIr4pc1leQVjwmSXIMSmRvrTr5hUwZaxG3GExKI= +github.com/vicanso/go-charts/v2 v2.6.0/go.mod h1:aEuuwzCT+p/Pd8YnAPMEV+sMKjpKK6anH1u6CxiboIw= +github.com/vicanso/hes v0.6.1 h1:BRGUDhV2sJYMieJf4dgxFXjvuhDgUWBsINELcti0Z8M= +github.com/vicanso/hes v0.6.1/go.mod h1:awwBbvcDTk8APxRmiV7Hxrir89/iCxgB6RMeLc5toh0= +github.com/vicanso/intranet-ip v0.1.0 h1:UeoxilO2VDIkeZZxmu6aT+f5o79mfGdsSdwoEv75nYo= +github.com/vicanso/intranet-ip v0.1.0/go.mod h1:N1yrHdDYWNsOs5V374DuAJHba+d2dxUDcjVALgIlfOg= +github.com/vicanso/keygrip v1.2.1 h1:876fXDwGJqxdi4JxZ1lNGBxYswyLZotrs7AA2QWcLeY= +github.com/vicanso/keygrip v1.2.1/go.mod h1:tfB5az1yqold78zotkzNugk3sV+QW5m71CFz3zg9eeo= +github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grid.go b/grid.go deleted file mode 100644 index 0ebd226..0000000 --- a/grid.go +++ /dev/null @@ -1,92 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -type gridPainter struct { - p *Painter - opt *GridPainterOption -} - -type GridPainterOption struct { - // The stroke width - StrokeWidth float64 - // The stroke color - StrokeColor Color - // The spans of column - ColumnSpans []int - // The column of grid - Column int - // The row of grid - Row int - // Ignore first row - IgnoreFirstRow bool - // Ignore last row - IgnoreLastRow bool - // Ignore first column - IgnoreFirstColumn bool - // Ignore last column - IgnoreLastColumn bool -} - -// NewGridPainter returns new a grid renderer -func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter { - return &gridPainter{ - p: p, - opt: &opt, - } -} - -func (g *gridPainter) Render() (Box, error) { - opt := g.opt - ignoreColumnLines := make([]int, 0) - if opt.IgnoreFirstColumn { - ignoreColumnLines = append(ignoreColumnLines, 0) - } - if opt.IgnoreLastColumn { - ignoreColumnLines = append(ignoreColumnLines, opt.Column) - } - ignoreRowLines := make([]int, 0) - if opt.IgnoreFirstRow { - ignoreRowLines = append(ignoreRowLines, 0) - } - if opt.IgnoreLastRow { - ignoreRowLines = append(ignoreRowLines, opt.Row) - } - strokeWidth := opt.StrokeWidth - if strokeWidth <= 0 { - strokeWidth = 1 - } - - g.p.SetDrawingStyle(Style{ - StrokeWidth: strokeWidth, - StrokeColor: opt.StrokeColor, - }) - g.p.Grid(GridOption{ - Column: opt.Column, - ColumnSpans: opt.ColumnSpans, - Row: opt.Row, - IgnoreColumnLines: ignoreColumnLines, - IgnoreRowLines: ignoreRowLines, - }) - return g.p.box, nil -} diff --git a/grid_test.go b/grid_test.go deleted file mode 100644 index fa9c3a6..0000000 --- a/grid_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestGrid(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewGridPainter(p, GridPainterOption{ - StrokeColor: drawing.ColorBlack, - Column: 6, - Row: 6, - IgnoreFirstRow: true, - IgnoreLastRow: true, - IgnoreFirstColumn: true, - IgnoreLastColumn: true, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewGridPainter(p, GridPainterOption{ - StrokeColor: drawing.ColorBlack, - ColumnSpans: []int{ - 2, - 5, - 3, - }, - Row: 6, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go deleted file mode 100644 index ed091c9..0000000 --- a/horizontal_bar_chart.go +++ /dev/null @@ -1,216 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type horizontalBarChart struct { - p *Painter - opt *HorizontalBarChartOption -} - -type HorizontalBarChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The x axis option - XAxis XAxisOption - // The padding of line chart - Padding Box - // The y axis option - YAxisOptions []YAxisOption - // The option of title - Title TitleOption - // The legend option - Legend LegendOption - BarHeight int - // Margin of bar - BarMargin int -} - -// NewHorizontalBarChart returns a horizontal bar chart renderer -func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &horizontalBarChart{ - p: p, - opt: &opt, - } -} - -func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - p := h.p - opt := h.opt - seriesPainter := result.seriesPainter - yRange := result.axisRanges[0] - y0, y1 := yRange.GetRange(0) - height := int(y1 - y0) - // 每一块之间的margin - margin := 10 - // 每一个bar之间的margin - barMargin := 5 - if height < 20 { - margin = 2 - barMargin = 2 - } else if height < 50 { - margin = 5 - barMargin = 3 - } - if opt.BarMargin > 0 { - barMargin = opt.BarMargin - } - seriesCount := len(seriesList) - // 总的高度-两个margin-(总数-1)的barMargin - barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / seriesCount - if opt.BarHeight > 0 && opt.BarHeight < barHeight { - barHeight = opt.BarHeight - margin = (height - seriesCount*barHeight - barMargin*(seriesCount-1)) / 2 - } - - theme := opt.Theme - - max, min := seriesList.GetMaxMin(0) - xRange := NewRange(AxisRangeOption{ - Painter: p, - Min: min, - Max: max, - DivideCount: defaultAxisDivideCount, - Size: seriesPainter.Width(), - }) - seriesNames := seriesList.Names() - - rendererList := []Renderer{} - for index := range seriesList { - series := seriesList[index] - seriesColor := theme.GetSeriesColor(series.index) - divideValues := yRange.AutoDivide() - - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } - for j, item := range series.Data { - if j >= yRange.divideCount { - continue - } - // 显示位置切换 - j = yRange.divideCount - j - 1 - y := divideValues[j] - y += margin - if index != 0 { - y += index * (barHeight + barMargin) - } - - w := int(xRange.getHeight(item.Value)) - fillColor := seriesColor - if !item.Style.FillColor.IsZero() { - fillColor = item.Style.FillColor - } - right := w - if series.RoundRadius <= 0 { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).Rect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }) - } else { - seriesPainter.OverrideDrawingStyle(Style{ - FillColor: fillColor, - }).RoundedRect(chart.Box{ - Top: y, - Left: 0, - Right: right, - Bottom: y + barHeight, - }, series.RoundRadius) - } - - // 如果label不需要展示,则返回 - if labelPainter == nil { - continue - } - labelValue := LabelValue{ - Orient: OrientHorizontal, - Index: index, - Value: item.Value, - X: right, - Y: y + barHeight>>1, - Offset: series.Label.Offset, - FontColor: series.Label.Color, - FontSize: series.Label.FontSize, - } - if series.Label.Position == PositionLeft { - labelValue.X = 0 - if labelValue.FontColor.IsZero() { - if isLightColor(fillColor) { - labelValue.FontColor = defaultLightFontColor - } else { - labelValue.FontColor = defaultDarkFontColor - } - } - } - labelPainter.Add(labelValue) - } - } - err := doRender(rendererList...) - if err != nil { - return BoxZero, err - } - return p.box, nil -} - -func (h *horizontalBarChart) Render() (Box, error) { - p := h.p - opt := h.opt - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - axisReversed: true, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypeHorizontalBar) - return h.render(renderResult, seriesList) -} diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go deleted file mode 100644 index e078c4a..0000000 --- a/horizontal_bar_chart_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHorizontalBarChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ - Padding: Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, - SeriesList: NewSeriesListDataFromValues([][]float64{ - { - 18203, - 23489, - 29034, - 104970, - 131744, - 630230, - }, - { - 19325, - 23438, - 31000, - 121594, - 134141, - 681807, - }, - }, ChartTypeHorizontalBar), - Title: TitleOption{ - Text: "World Population", - }, - Legend: NewLegendOption([]string{ - "2011", - "2012", - }), - YAxisOptions: NewYAxisOptions([]string{ - "Brazil", - "Indonesia", - "USA", - "India", - "China", - "World", - }), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n20112012World PopulationWorldChinaIndiaUSAIndonesiaBrazil0122.28k244.56k366.84k489.12k611.4k733.68k", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/legend.go b/legend.go deleted file mode 100644 index 035642c..0000000 --- a/legend.go +++ /dev/null @@ -1,251 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strconv" - "strings" -) - -type legendPainter struct { - p *Painter - opt *LegendOption -} - -const IconRect = "rect" -const IconLineDot = "lineDot" - -type LegendOption struct { - // The theme - Theme ColorPalette - // Text array of legend - Data []string - // Distance between legend component and the left side of the container. - // It can be pixel value: 20, percentage value: 20%, - // or position value: right, center. - Left string - // Distance between legend component and the top side of the container. - // It can be pixel value: 20. - Top string - // Legend marker and text aligning, it can be left or right, default is left. - Align string - // The layout orientation of legend, it can be horizontal or vertical, default is horizontal. - Orient string - // Icon of the legend. - Icon string - // Font size of legend text - FontSize float64 - // FontColor color of legend text - FontColor Color - // The flag for show legend, set this to *false will hide legend - Show *bool - // The padding of legend - Padding Box -} - -// NewLegendOption returns a legend option -func NewLegendOption(labels []string, left ...string) LegendOption { - opt := LegendOption{ - Data: labels, - } - if len(left) != 0 { - opt.Left = left[0] - } - return opt -} - -// IsEmpty checks legend is empty -func (opt *LegendOption) IsEmpty() bool { - isEmpty := true - for _, v := range opt.Data { - if v != "" { - isEmpty = false - break - } - } - return isEmpty -} - -// NewLegendPainter returns a legend renderer -func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter { - return &legendPainter{ - p: p, - opt: &opt, - } -} - -func (l *legendPainter) Render() (Box, error) { - opt := l.opt - theme := opt.Theme - if opt.IsEmpty() || - isFalse(opt.Show) { - return BoxZero, nil - } - if theme == nil { - theme = l.p.theme - } - if opt.FontSize == 0 { - opt.FontSize = theme.GetFontSize() - } - if opt.FontColor.IsZero() { - opt.FontColor = theme.GetTextColor() - } - if opt.Left == "" { - opt.Left = PositionCenter - } - padding := opt.Padding - if padding.IsZero() { - padding.Top = 5 - } - p := l.p.Child(PainterPaddingOption(padding)) - p.SetTextStyle(Style{ - FontSize: opt.FontSize, - FontColor: opt.FontColor, - }) - measureList := make([]Box, len(opt.Data)) - maxTextWidth := 0 - for index, text := range opt.Data { - b := p.MeasureText(text) - if b.Width() > maxTextWidth { - maxTextWidth = b.Width() - } - measureList[index] = b - } - - // 计算展示的宽高 - width := 0 - height := 0 - offset := 20 - textOffset := 2 - legendWidth := 30 - legendHeight := 20 - itemMaxHeight := 0 - for _, item := range measureList { - if item.Height() > itemMaxHeight { - itemMaxHeight = item.Height() - } - if opt.Orient == OrientVertical { - height += item.Height() - } else { - width += item.Width() - } - } - // 增加padding - itemMaxHeight += 10 - if opt.Orient == OrientVertical { - width = maxTextWidth + textOffset + legendWidth - height = offset * len(opt.Data) - } else { - height = legendHeight - offsetValue := (len(opt.Data) - 1) * (offset + textOffset) - allLegendWidth := len(opt.Data) * legendWidth - width += (offsetValue + allLegendWidth) - } - - // 计算开始的位置 - left := 0 - switch opt.Left { - case PositionRight: - left = p.Width() - width - case PositionCenter: - left = (p.Width() - width) >> 1 - default: - if strings.HasSuffix(opt.Left, "%") { - value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) - left = p.Width() * value / 100 - } else { - value, _ := strconv.Atoi(opt.Left) - left = value - } - } - top, _ := strconv.Atoi(opt.Top) - - if left < 0 { - left = 0 - } - - x := int(left) - y := int(top) + 10 - startY := y - x0 := x - y0 := y - - drawIcon := func(top, left int) int { - if opt.Icon == IconRect { - p.Rect(Box{ - Top: top - legendHeight + 8, - Left: left, - Right: left + legendWidth, - Bottom: top + 1, - }) - } else { - p.LegendLineDot(Box{ - Top: top + 1, - Left: left, - Right: left + legendWidth, - Bottom: top + legendHeight + 1, - }) - } - return left + legendWidth - } - lastIndex := len(opt.Data) - 1 - for index, text := range opt.Data { - color := theme.GetSeriesColor(index) - p.SetDrawingStyle(Style{ - FillColor: color, - StrokeColor: color, - }) - itemWidth := x0 + measureList[index].Width() + textOffset + offset + legendWidth - if lastIndex == index { - itemWidth = x0 + measureList[index].Width() + legendWidth - } - if itemWidth > p.Width() { - x0 = 0 - y += itemMaxHeight - y0 = y - } - if opt.Align != AlignRight { - x0 = drawIcon(y0, x0) - x0 += textOffset - } - p.Text(text, x0, y0) - x0 += measureList[index].Width() - if opt.Align == AlignRight { - x0 += textOffset - x0 = drawIcon(y0, x0) - } - if opt.Orient == OrientVertical { - y0 += offset - x0 = x - } else { - x0 += offset - y0 = y - } - height = y0 - startY + 10 - } - - return Box{ - Right: width, - Bottom: height + padding.Bottom + padding.Top, - }, nil -} diff --git a/legend_test.go b/legend_test.go deleted file mode 100644 index 526f178..0000000 --- a/legend_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewLegend(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewLegendPainter(p, LegendOption{ - Data: []string{ - "One", - "Two", - "Three", - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nOneTwoThree", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewLegendPainter(p, LegendOption{ - Data: []string{ - "One", - "Two", - "Three", - }, - Left: PositionLeft, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nOneTwoThree", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewLegendPainter(p, LegendOption{ - Data: []string{ - "One", - "Two", - "Three", - }, - Orient: OrientVertical, - Icon: IconRect, - Left: "10%", - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nOneTwoThree", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/line_chart.go b/line_chart.go deleted file mode 100644 index fb1d16a..0000000 --- a/line_chart.go +++ /dev/null @@ -1,240 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -type lineChart struct { - p *Painter - opt *LineChartOption -} - -// NewLineChart returns a line chart render -func NewLineChart(p *Painter, opt LineChartOption) *lineChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &lineChart{ - p: p, - opt: &opt, - } -} - -type LineChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The x axis option - XAxis XAxisOption - // The padding of line chart - Padding Box - // The y axis option - YAxisOptions []YAxisOption - // The option of title - Title TitleOption - // The legend option - Legend LegendOption - // The flag for show symbol of line, set this to *false will hide symbol - SymbolShow *bool - // The stroke width of line - StrokeWidth float64 - // Fill the area of line - FillArea bool - // background is filled - backgroundIsFilled bool - // background fill (alpha) opacity - Opacity uint8 -} - -func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - p := l.p - opt := l.opt - boundaryGap := true - if isFalse(opt.XAxis.BoundaryGap) { - boundaryGap = false - } - - seriesPainter := result.seriesPainter - - xDivideCount := len(opt.XAxis.Data) - if !boundaryGap { - xDivideCount-- - } - xDivideValues := autoDivide(seriesPainter.Width(), xDivideCount) - xValues := make([]int, len(xDivideValues)-1) - if boundaryGap { - for i := 0; i < len(xDivideValues)-1; i++ { - xValues[i] = (xDivideValues[i] + xDivideValues[i+1]) >> 1 - } - } else { - xValues = xDivideValues - } - markPointPainter := NewMarkPointPainter(seriesPainter) - markLinePainter := NewMarkLinePainter(seriesPainter) - rendererList := []Renderer{ - markPointPainter, - markLinePainter, - } - strokeWidth := opt.StrokeWidth - if strokeWidth == 0 { - strokeWidth = defaultStrokeWidth - } - seriesNames := seriesList.Names() - for index := range seriesList { - series := seriesList[index] - seriesColor := opt.Theme.GetSeriesColor(series.index) - drawingStyle := Style{ - StrokeColor: seriesColor, - StrokeWidth: strokeWidth, - } - if len(series.Style.StrokeDashArray) > 0 { - drawingStyle.StrokeDashArray = series.Style.StrokeDashArray - } - - yRange := result.axisRanges[series.AxisIndex] - points := make([]Point, 0) - var labelPainter *SeriesLabelPainter - if series.Label.Show { - labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{ - P: seriesPainter, - SeriesNames: seriesNames, - Label: series.Label, - Theme: opt.Theme, - Font: opt.Font, - }) - rendererList = append(rendererList, labelPainter) - } - for i, item := range series.Data { - h := yRange.getRestHeight(item.Value) - if item.Value == nullValue { - h = int(math.MaxInt32) - } - p := Point{ - X: xValues[i], - Y: h, - } - points = append(points, p) - - // 如果label不需要展示,则返回 - if labelPainter == nil { - continue - } - labelPainter.Add(LabelValue{ - Index: index, - Value: item.Value, - X: p.X, - Y: p.Y, - // 字体大小 - FontSize: series.Label.FontSize, - }) - } - // 如果需要填充区域 - if opt.FillArea { - areaPoints := make([]Point, len(points)) - copy(areaPoints, points) - bottomY := yRange.getRestHeight(yRange.min) - var opacity uint8 = 200 - if opt.Opacity != 0 { - opacity = opt.Opacity - } - areaPoints = append(areaPoints, Point{ - X: areaPoints[len(areaPoints)-1].X, - Y: bottomY, - }, Point{ - X: areaPoints[0].X, - Y: bottomY, - }, areaPoints[0]) - seriesPainter.SetDrawingStyle(Style{ - FillColor: seriesColor.WithAlpha(opacity), - }) - seriesPainter.FillArea(areaPoints) - } - seriesPainter.SetDrawingStyle(drawingStyle) - - // 画线 - seriesPainter.LineStroke(points) - - // 画点 - if opt.Theme.IsDark() { - drawingStyle.FillColor = drawingStyle.StrokeColor - } else { - drawingStyle.FillColor = drawing.ColorWhite - } - drawingStyle.StrokeWidth = 1 - seriesPainter.SetDrawingStyle(drawingStyle) - if !isFalse(opt.SymbolShow) { - seriesPainter.Dots(points) - } - markPointPainter.Add(markPointRenderOption{ - FillColor: seriesColor, - Font: opt.Font, - Points: points, - Series: series, - }) - markLinePainter.Add(markLineRenderOption{ - FillColor: seriesColor, - FontColor: opt.Theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: series, - Range: yRange, - }) - } - // 最大、最小的mark point - err := doRender(rendererList...) - if err != nil { - return BoxZero, err - } - - return p.box, nil -} - -func (l *lineChart) Render() (Box, error) { - p := l.p - opt := l.opt - - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - backgroundIsFilled: opt.backgroundIsFilled, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypeLine) - - return l.render(renderResult, seriesList) -} diff --git a/line_chart_test.go b/line_chart_test.go deleted file mode 100644 index e169f90..0000000 --- a/line_chart_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLineChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - { - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }, - { - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }, - { - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }, - { - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }, - } - _, err := NewLineChart(p, LineChartOption{ - Title: TitleOption{ - Text: "Line", - }, - Padding: Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - Legend: NewLegendOption([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, PositionCenter), - SeriesList: NewSeriesListDataFromValues(values), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", - }, - { - render: func(p *Painter) ([]byte, error) { - values := [][]float64{ - { - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }, - { - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }, - { - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }, - { - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }, - { - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }, - } - _, err := NewLineChart(p, LineChartOption{ - Title: TitleOption{ - Text: "Line", - }, - Padding: Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, - XAxis: NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, FalseFlag()), - Legend: NewLegendOption([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, PositionCenter), - SeriesList: NewSeriesListDataFromValues(values), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEmailUnion AdsVideo AdsDirectSearch EngineLine1.44k1.2k9607204802400MonTueWedThuFriSatSun", - }, - } - - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..113a3ed --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + "embed" + "encoding/base64" + "fmt" + "time" + + "github.com/vicanso/elton" + "github.com/vicanso/elton/middleware" + "github.com/vicanso/go-charts/v2" +) + +//go:embed web +var webFS embed.FS + +func main() { + e := elton.New() + + e.Use(middleware.NewLogger(middleware.LoggerConfig{ + Format: `{real-ip} {when-iso} "{method} {uri} {proto}" {status} {size-human} "{userAgent}"`, + OnLog: func(s string, _ *elton.Context) { + fmt.Println(s) + }, + })) + e.Use(middleware.NewDefaultError()) + e.Use(middleware.NewDefaultBodyParser()) + e.Use(func(c *elton.Context) error { + c.NoCache() + return c.Next() + }) + + assetFS := middleware.NewEmbedStaticFS(webFS, "web") + e.GET("/static/*", middleware.NewStaticServe(assetFS, middleware.StaticServeConfig{ + // 客户端缓存 + MaxAge: 10 * time.Minute, + // 缓存服务器缓存 + SMaxAge: 5 * time.Minute, + DenyQueryString: true, + DisableLastModified: true, + EnableStrongETag: true, + })) + + e.GET("/ping", func(c *elton.Context) error { + c.BodyBuffer = bytes.NewBufferString("pong") + return nil + }) + + e.GET("/", func(c *elton.Context) error { + buf, err := webFS.ReadFile("web/index.html") + if err != nil { + return err + } + c.SetContentTypeByExt(".html") + c.BodyBuffer = bytes.NewBuffer(buf) + return nil + }) + e.POST("/", func(c *elton.Context) error { + outputType := c.QueryParam("outputType") + fn := charts.RenderEChartsToSVG + isPNG := false + if outputType == "png" { + isPNG = true + fn = charts.RenderEChartsToPNG + } + buf, err := fn(string(c.RequestBody)) + if err != nil { + return err + } + if isPNG { + buf = []byte(base64.StdEncoding.EncodeToString(buf)) + } + c.BodyBuffer = bytes.NewBuffer(buf) + return nil + }) + + err := e.ListenAndServe(":7001") + if err != nil { + panic(err) + } +} diff --git a/mark_line.go b/mark_line.go deleted file mode 100644 index bc850bb..0000000 --- a/mark_line.go +++ /dev/null @@ -1,113 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" -) - -// NewMarkLine returns a series mark line -func NewMarkLine(markLineTypes ...string) SeriesMarkLine { - data := make([]SeriesMarkData, len(markLineTypes)) - for index, t := range markLineTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkLine{ - Data: data, - } -} - -type markLinePainter struct { - p *Painter - options []markLineRenderOption -} - -func (m *markLinePainter) Add(opt markLineRenderOption) { - m.options = append(m.options, opt) -} - -// NewMarkLinePainter returns a mark line renderer -func NewMarkLinePainter(p *Painter) *markLinePainter { - return &markLinePainter{ - p: p, - options: make([]markLineRenderOption, 0), - } -} - -type markLineRenderOption struct { - FillColor Color - FontColor Color - StrokeColor Color - Font *truetype.Font - Series Series - Range axisRange -} - -func (m *markLinePainter) Render() (Box, error) { - painter := m.p - for _, opt := range m.options { - s := opt.Series - if len(s.MarkLine.Data) == 0 { - continue - } - font := opt.Font - if font == nil { - font, _ = GetDefaultFont() - } - summary := s.Summary() - for _, markLine := range s.MarkLine.Data { - // 由于mark line会修改style,因此每次重新设置 - painter.OverrideDrawingStyle(Style{ - FillColor: opt.FillColor, - StrokeColor: opt.StrokeColor, - StrokeWidth: 1, - StrokeDashArray: []float64{ - 4, - 2, - }, - }).OverrideTextStyle(Style{ - Font: font, - FontColor: opt.FontColor, - FontSize: labelFontSize, - }) - value := float64(0) - switch markLine.Type { - case SeriesMarkDataTypeMax: - value = summary.MaxValue - case SeriesMarkDataTypeMin: - value = summary.MinValue - default: - value = summary.AverageValue - } - y := opt.Range.getRestHeight(value) - width := painter.Width() - text := commafWithDigits(value) - textBox := painter.MeasureText(text) - painter.MarkLine(0, y, width-2) - painter.Text(text, width, y+textBox.Height()>>1-2) - } - } - return BoxZero, nil -} diff --git a/mark_line_test.go b/mark_line_test.go deleted file mode 100644 index 0448cda..0000000 --- a/mark_line_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestMarkLine(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - markLine := NewMarkLinePainter(p) - series := NewSeriesFromValues([]float64{ - 1, - 2, - 3, - }) - series.MarkLine = NewMarkLine( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeAverage, - SeriesMarkDataTypeMin, - ) - markLine.Add(markLineRenderOption{ - FillColor: drawing.ColorBlack, - FontColor: drawing.ColorBlack, - StrokeColor: drawing.ColorBlack, - Series: series, - Range: NewRange(AxisRangeOption{ - Painter: p, - Min: 0, - Max: 5, - Size: p.Height(), - DivideCount: 6, - }), - }) - _, err := markLine.Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n321", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/mark_point.go b/mark_point.go deleted file mode 100644 index fd8a88b..0000000 --- a/mark_point.go +++ /dev/null @@ -1,115 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" -) - -// NewMarkPoint returns a series mark point -func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { - data := make([]SeriesMarkData, len(markPointTypes)) - for index, t := range markPointTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkPoint{ - Data: data, - } -} - -type markPointPainter struct { - p *Painter - options []markPointRenderOption -} - -func (m *markPointPainter) Add(opt markPointRenderOption) { - m.options = append(m.options, opt) -} - -type markPointRenderOption struct { - FillColor Color - Font *truetype.Font - Series Series - Points []Point -} - -// NewMarkPointPainter returns a mark point renderer -func NewMarkPointPainter(p *Painter) *markPointPainter { - return &markPointPainter{ - p: p, - options: make([]markPointRenderOption, 0), - } -} - -func (m *markPointPainter) Render() (Box, error) { - painter := m.p - for _, opt := range m.options { - s := opt.Series - if len(s.MarkPoint.Data) == 0 { - continue - } - points := opt.Points - summary := s.Summary() - symbolSize := s.MarkPoint.SymbolSize - if symbolSize == 0 { - symbolSize = 30 - } - textStyle := Style{ - FontSize: labelFontSize, - StrokeWidth: 1, - Font: opt.Font, - } - if isLightColor(opt.FillColor) { - textStyle.FontColor = defaultLightFontColor - } else { - textStyle.FontColor = defaultDarkFontColor - } - painter.OverrideDrawingStyle(Style{ - FillColor: opt.FillColor, - }).OverrideTextStyle(textStyle) - for _, markPointData := range s.MarkPoint.Data { - textStyle.FontSize = labelFontSize - painter.OverrideTextStyle(textStyle) - p := points[summary.MinIndex] - value := summary.MinValue - switch markPointData.Type { - case SeriesMarkDataTypeMax: - p = points[summary.MaxIndex] - value = summary.MaxValue - } - - painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize) - text := commafWithDigits(value) - textBox := painter.MeasureText(text) - if textBox.Width() > symbolSize { - textStyle.FontSize = smallLabelFontSize - painter.OverrideTextStyle(textStyle) - textBox = painter.MeasureText(text) - } - painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) - } - } - return BoxZero, nil -} diff --git a/mark_point_test.go b/mark_point_test.go deleted file mode 100644 index 298345b..0000000 --- a/mark_point_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestMarkPoint(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - series := NewSeriesFromValues([]float64{ - 1, - 2, - 3, - }) - series.MarkPoint = NewMarkPoint(SeriesMarkDataTypeMax) - markPoint := NewMarkPointPainter(p) - markPoint.Add(markPointRenderOption{ - FillColor: drawing.ColorBlack, - Series: series, - Points: []Point{ - { - X: 10, - Y: 10, - }, - { - X: 30, - Y: 30, - }, - { - X: 50, - Y: 50, - }, - }, - }) - _, err := markPoint.Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\n3", - }, - } - - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/painter.go b/painter.go deleted file mode 100644 index bee646f..0000000 --- a/painter.go +++ /dev/null @@ -1,866 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "bytes" - "errors" - "math" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type ValueFormatter func(float64) string - -type Painter struct { - render chart.Renderer - box Box - font *truetype.Font - parent *Painter - style Style - theme ColorPalette - // 类型 - outputType string - valueFormatter ValueFormatter -} - -type PainterOptions struct { - // Draw type, "svg" or "png", default type is "png" - Type string - // The width of draw painter - Width int - // The height of draw painter - Height int - // The font for painter - Font *truetype.Font -} - -type PainterOption func(*Painter) - -type TicksOption struct { - // the first tick - First int - Length int - Orient string - Count int - Unit int -} - -type MultiTextOption struct { - TextList []string - Orient string - Unit int - Position string - Align string - // The text rotation of label - TextRotation float64 - Offset Box - // The first text index - First int -} - -type GridOption struct { - Column int - Row int - ColumnSpans []int - // 忽略不展示的column - IgnoreColumnLines []int - // 忽略不展示的row - IgnoreRowLines []int -} - -// PainterPaddingOption sets the padding of draw painter -func PainterPaddingOption(padding Box) PainterOption { - return func(p *Painter) { - p.box.Left += padding.Left - p.box.Top += padding.Top - p.box.Right -= padding.Right - p.box.Bottom -= padding.Bottom - } -} - -// PainterBoxOption sets the box of draw painter -func PainterBoxOption(box Box) PainterOption { - return func(p *Painter) { - if box.IsZero() { - return - } - p.box = box - } -} - -// PainterFontOption sets the font of draw painter -func PainterFontOption(font *truetype.Font) PainterOption { - return func(p *Painter) { - if font == nil { - return - } - p.font = font - } -} - -// PainterStyleOption sets the style of draw painter -func PainterStyleOption(style Style) PainterOption { - return func(p *Painter) { - p.SetStyle(style) - } -} - -// PainterThemeOption sets the theme of draw painter -func PainterThemeOption(theme ColorPalette) PainterOption { - return func(p *Painter) { - if theme == nil { - return - } - p.theme = theme - } -} - -// PainterWidthHeightOption set width or height of draw painter -func PainterWidthHeightOption(width, height int) PainterOption { - return func(p *Painter) { - if width > 0 { - p.box.Right = p.box.Left + width - } - if height > 0 { - p.box.Bottom = p.box.Top + height - } - } -} - -// NewPainter creates a painter -func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { - if opts.Width <= 0 || opts.Height <= 0 { - return nil, errors.New("width/height can not be nil") - } - font := opts.Font - if font == nil { - f, err := GetDefaultFont() - if err != nil { - return nil, err - } - font = f - } - fn := chart.PNG - if opts.Type == ChartOutputSVG { - fn = chart.SVG - } - width := opts.Width - height := opts.Height - r, err := fn(width, height) - if err != nil { - return nil, err - } - r.SetFont(font) - - p := &Painter{ - render: r, - box: Box{ - Right: opts.Width, - Bottom: opts.Height, - }, - font: font, - // 类型 - outputType: opts.Type, - } - p.setOptions(opt...) - if p.theme == nil { - p.theme = NewTheme(ThemeLight) - } - return p, nil -} -func (p *Painter) setOptions(opts ...PainterOption) { - for _, fn := range opts { - fn(p) - } -} - -func (p *Painter) Child(opt ...PainterOption) *Painter { - child := &Painter{ - // 格式化 - valueFormatter: p.valueFormatter, - // render - render: p.render, - box: p.box.Clone(), - font: p.font, - parent: p, - style: p.style, - theme: p.theme, - } - child.setOptions(opt...) - return child -} - -func (p *Painter) SetStyle(style Style) { - if style.Font == nil { - style.Font = p.font - } - p.style = style - style.WriteToRenderer(p.render) -} - -func overrideStyle(defaultStyle Style, style Style) Style { - if style.StrokeWidth == 0 { - style.StrokeWidth = defaultStyle.StrokeWidth - } - if style.StrokeColor.IsZero() { - style.StrokeColor = defaultStyle.StrokeColor - } - if style.StrokeDashArray == nil { - style.StrokeDashArray = defaultStyle.StrokeDashArray - } - if style.DotColor.IsZero() { - style.DotColor = defaultStyle.DotColor - } - if style.DotWidth == 0 { - style.DotWidth = defaultStyle.DotWidth - } - if style.FillColor.IsZero() { - style.FillColor = defaultStyle.FillColor - } - if style.FontSize == 0 { - style.FontSize = defaultStyle.FontSize - } - if style.FontColor.IsZero() { - style.FontColor = defaultStyle.FontColor - } - if style.Font == nil { - style.Font = defaultStyle.Font - } - return style -} - -func (p *Painter) OverrideDrawingStyle(style Style) *Painter { - s := overrideStyle(p.style, style) - p.SetDrawingStyle(s) - return p -} - -func (p *Painter) SetDrawingStyle(style Style) *Painter { - style.WriteDrawingOptionsToRenderer(p.render) - return p -} - -func (p *Painter) SetTextStyle(style Style) *Painter { - if style.Font == nil { - style.Font = p.font - } - style.WriteTextOptionsToRenderer(p.render) - return p -} -func (p *Painter) OverrideTextStyle(style Style) *Painter { - s := overrideStyle(p.style, style) - p.SetTextStyle(s) - return p -} - -func (p *Painter) ResetStyle() *Painter { - p.style.WriteToRenderer(p.render) - return p -} - -// Bytes returns the data of draw canvas -func (p *Painter) Bytes() ([]byte, error) { - buffer := bytes.Buffer{} - err := p.render.Save(&buffer) - if err != nil { - return nil, err - } - return buffer.Bytes(), err -} - -// MoveTo moves the cursor to a given point -func (p *Painter) MoveTo(x, y int) *Painter { - p.render.MoveTo(x+p.box.Left, y+p.box.Top) - return p -} - -func (p *Painter) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) *Painter { - p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta) - return p -} - -func (p *Painter) LineTo(x, y int) *Painter { - p.render.LineTo(x+p.box.Left, y+p.box.Top) - return p -} - -func (p *Painter) QuadCurveTo(cx, cy, x, y int) *Painter { - p.render.QuadCurveTo(cx+p.box.Left, cy+p.box.Top, x+p.box.Left, y+p.box.Top) - return p -} - -func (p *Painter) Pin(x, y, width int) *Painter { - r := float64(width) / 2 - y -= width / 4 - angle := chart.DegreesToRadians(15) - box := p.box - - startAngle := math.Pi/2 + angle - delta := 2*math.Pi - 2*angle - p.ArcTo(x, y, r, r, startAngle, delta) - p.LineTo(x, y) - p.Close() - p.FillStroke() - - startX := x - int(r) - startY := y - endX := x + int(r) - endY := y - p.MoveTo(startX, startY) - - left := box.Left - top := box.Top - cx := x - cy := y + int(r*2.5) - p.render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top) - p.Close() - p.Fill() - return p -} - -func (p *Painter) arrow(x, y, width, height int, direction string) *Painter { - halfWidth := width >> 1 - halfHeight := height >> 1 - if direction == PositionTop || direction == PositionBottom { - x0 := x - halfWidth - x1 := x0 + width - dy := -height / 3 - y0 := y - y1 := y0 - height - if direction == PositionBottom { - y0 = y - height - y1 = y - dy = 2 * dy - } - p.MoveTo(x0, y0) - p.LineTo(x0+halfWidth, y1) - p.LineTo(x1, y0) - p.LineTo(x0+halfWidth, y+dy) - p.LineTo(x0, y0) - } else { - x0 := x + width - x1 := x0 - width - y0 := y - halfHeight - dx := -width / 3 - if direction == PositionRight { - x0 = x - width - dx = -dx - x1 = x0 + width - } - p.MoveTo(x0, y0) - p.LineTo(x1, y0+halfHeight) - p.LineTo(x0, y0+height) - p.LineTo(x0+dx, y0+halfHeight) - p.LineTo(x0, y0) - } - p.FillStroke() - return p -} - -func (p *Painter) ArrowLeft(x, y, width, height int) *Painter { - p.arrow(x, y, width, height, PositionLeft) - return p -} - -func (p *Painter) ArrowRight(x, y, width, height int) *Painter { - p.arrow(x, y, width, height, PositionRight) - return p -} - -func (p *Painter) ArrowTop(x, y, width, height int) *Painter { - p.arrow(x, y, width, height, PositionTop) - return p -} -func (p *Painter) ArrowBottom(x, y, width, height int) *Painter { - p.arrow(x, y, width, height, PositionBottom) - return p -} - -func (p *Painter) Circle(radius float64, x, y int) *Painter { - p.render.Circle(radius, x+p.box.Left, y+p.box.Top) - return p -} - -func (p *Painter) Stroke() *Painter { - p.render.Stroke() - return p -} - -func (p *Painter) Close() *Painter { - p.render.Close() - return p -} - -func (p *Painter) FillStroke() *Painter { - p.render.FillStroke() - return p -} - -func (p *Painter) Fill() *Painter { - p.render.Fill() - return p -} - -func (p *Painter) Width() int { - return p.box.Width() -} - -func (p *Painter) Height() int { - return p.box.Height() -} - -func (p *Painter) MeasureText(text string) Box { - return p.render.MeasureText(text) -} - -func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) { - maxWidth := 0 - maxHeight := 0 - for _, text := range textList { - box := p.MeasureText(text) - if maxWidth < box.Width() { - maxWidth = box.Width() - } - if maxHeight < box.Height() { - maxHeight = box.Height() - } - } - return maxWidth, maxHeight -} - -func (p *Painter) LineStroke(points []Point) *Painter { - shouldMoveTo := false - for index, point := range points { - x := point.X - y := point.Y - if y == int(math.MaxInt32) { - p.Stroke() - shouldMoveTo = true - continue - } - if shouldMoveTo || index == 0 { - p.MoveTo(x, y) - shouldMoveTo = false - } else { - p.LineTo(x, y) - } - } - p.Stroke() - return p -} - -func (p *Painter) SmoothLineStroke(points []Point) *Painter { - prevX := 0 - prevY := 0 - // TODO 如何生成平滑的折线 - for index, point := range points { - x := point.X - y := point.Y - if index == 0 { - p.MoveTo(x, y) - } else { - cx := prevX + (x-prevX)/5 - cy := y + (y-prevY)/2 - p.QuadCurveTo(cx, cy, x, y) - } - prevX = x - prevY = y - } - p.Stroke() - return p -} - -func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter { - r := p.render - s := chart.Style{ - FillColor: color, - } - // 背景色 - p.SetDrawingStyle(s) - defer p.ResetStyle() - if len(inside) != 0 && inside[0] { - p.MoveTo(0, 0) - p.LineTo(width, 0) - p.LineTo(width, height) - p.LineTo(0, height) - p.LineTo(0, 0) - } else { - // 设置背景色不使用box,因此不直接使用Painter - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) - } - p.FillStroke() - return p -} -func (p *Painter) MarkLine(x, y, width int) *Painter { - arrowWidth := 16 - arrowHeight := 10 - endX := x + width - radius := 3 - p.Circle(3, x+radius, y) - p.render.Fill() - p.MoveTo(x+radius*3, y) - p.LineTo(endX-arrowWidth, y) - p.Stroke() - p.ArrowRight(endX, y, arrowWidth, arrowHeight) - return p -} - -func (p *Painter) Polygon(center Point, radius float64, sides int) *Painter { - points := getPolygonPoints(center, radius, sides) - for i, item := range points { - if i == 0 { - p.MoveTo(item.X, item.Y) - } else { - p.LineTo(item.X, item.Y) - } - } - p.LineTo(points[0].X, points[0].Y) - p.Stroke() - return p -} - -func (p *Painter) FillArea(points []Point) *Painter { - var x, y int - for index, point := range points { - x = point.X - y = point.Y - if index == 0 { - p.MoveTo(x, y) - } else { - p.LineTo(x, y) - } - } - p.Fill() - return p -} - -func (p *Painter) Text(body string, x, y int) *Painter { - p.render.Text(body, x+p.box.Left, y+p.box.Top) - return p -} - -func (p *Painter) TextRotation(body string, x, y int, radians float64) { - p.render.SetTextRotation(radians) - p.render.Text(body, x+p.box.Left, y+p.box.Top) - p.render.ClearTextRotation() -} - -func (p *Painter) SetTextRotation(radians float64) { - p.render.SetTextRotation(radians) -} -func (p *Painter) ClearTextRotation() { - p.render.ClearTextRotation() -} - -func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chart.Box { - style := p.style - textWarp := style.TextWrap - style.TextWrap = chart.TextWrapWord - r := p.render - lines := chart.Text.WrapFit(r, body, width, style) - p.SetTextStyle(style) - var output chart.Box - - textAlign := "" - if len(textAligns) != 0 { - textAlign = textAligns[0] - } - for index, line := range lines { - if line == "" { - continue - } - x0 := x - y0 := y + output.Height() - lineBox := r.MeasureText(line) - switch textAlign { - case AlignRight: - x0 += width - lineBox.Width() - case AlignCenter: - x0 += (width - lineBox.Width()) >> 1 - } - p.Text(line, x0, y0) - output.Right = chart.MaxInt(lineBox.Right, output.Right) - output.Bottom += lineBox.Height() - if index < len(lines)-1 { - output.Bottom += +style.GetTextLineSpacing() - } - } - p.style.TextWrap = textWarp - return output -} - -func (p *Painter) Ticks(opt TicksOption) *Painter { - if opt.Count <= 0 || opt.Length <= 0 { - return p - } - count := opt.Count - first := opt.First - width := p.Width() - height := p.Height() - unit := 1 - if opt.Unit > 1 { - unit = opt.Unit - } - var values []int - isVertical := opt.Orient == OrientVertical - if isVertical { - values = autoDivide(height, count) - } else { - values = autoDivide(width, count) - } - for index, value := range values { - if index < first { - continue - } - if (index-first)%unit != 0 { - continue - } - if isVertical { - p.LineStroke([]Point{ - { - X: 0, - Y: value, - }, - { - X: opt.Length, - Y: value, - }, - }) - } else { - p.LineStroke([]Point{ - { - X: value, - Y: opt.Length, - }, - { - X: value, - Y: 0, - }, - }) - } - } - return p -} - -func (p *Painter) MultiText(opt MultiTextOption) *Painter { - if len(opt.TextList) == 0 { - return p - } - count := len(opt.TextList) - positionCenter := true - showIndex := opt.Unit / 2 - if containsString([]string{ - PositionLeft, - PositionTop, - }, opt.Position) { - positionCenter = false - count-- - // 非居中 - showIndex = 0 - } - width := p.Width() - height := p.Height() - var values []int - isVertical := opt.Orient == OrientVertical - if isVertical { - values = autoDivide(height, count) - } else { - values = autoDivide(width, count) - } - isTextRotation := opt.TextRotation != 0 - offset := opt.Offset - for index, text := range opt.TextList { - if index < opt.First { - continue - } - if opt.Unit != 0 && (index-opt.First)%opt.Unit != showIndex { - continue - } - if isTextRotation { - p.ClearTextRotation() - p.SetTextRotation(opt.TextRotation) - } - box := p.MeasureText(text) - start := values[index] - if positionCenter { - start = (values[index] + values[index+1]) >> 1 - } - x := 0 - y := 0 - if isVertical { - y = start + box.Height()>>1 - switch opt.Align { - case AlignRight: - x = width - box.Width() - case AlignCenter: - x = width - box.Width()>>1 - default: - x = 0 - } - } else { - x = start - box.Width()>>1 - } - x += offset.Left - y += offset.Top - p.Text(text, x, y) - } - if isTextRotation { - p.ClearTextRotation() - } - return p -} - -func (p *Painter) Grid(opt GridOption) *Painter { - width := p.Width() - height := p.Height() - drawLines := func(values []int, ignoreIndexList []int, isVertical bool) { - for index, v := range values { - if containsInt(ignoreIndexList, index) { - continue - } - x0 := 0 - y0 := 0 - x1 := 0 - y1 := 0 - if isVertical { - - x0 = v - x1 = v - y1 = height - } else { - x1 = width - y0 = v - y1 = v - } - p.LineStroke([]Point{ - { - X: x0, - Y: y0, - }, - { - X: x1, - Y: y1, - }, - }) - } - } - columnCount := sumInt(opt.ColumnSpans) - if columnCount == 0 { - columnCount = opt.Column - } - if columnCount > 0 { - values := autoDivideSpans(width, columnCount, opt.ColumnSpans) - drawLines(values, opt.IgnoreColumnLines, true) - } - if opt.Row > 0 { - values := autoDivide(height, opt.Row) - drawLines(values, opt.IgnoreRowLines, false) - } - return p -} - -func (p *Painter) Dots(points []Point) *Painter { - for _, item := range points { - p.Circle(2, item.X, item.Y) - } - p.FillStroke() - return p -} - -func (p *Painter) Rect(box Box) *Painter { - p.MoveTo(box.Left, box.Top) - p.LineTo(box.Right, box.Top) - p.LineTo(box.Right, box.Bottom) - p.LineTo(box.Left, box.Bottom) - p.LineTo(box.Left, box.Top) - p.FillStroke() - return p -} - -func (p *Painter) RoundedRect(box Box, radius int) *Painter { - r := (box.Right - box.Left) / 2 - if radius > r { - radius = r - } - rx := float64(radius) - ry := float64(radius) - p.MoveTo(box.Left+radius, box.Top) - p.LineTo(box.Right-radius, box.Top) - - cx := box.Right - radius - cy := box.Top + radius - // right top - p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2) - - p.LineTo(box.Right, box.Bottom-radius) - - // right bottom - cx = box.Right - radius - cy = box.Bottom - radius - p.ArcTo(cx, cy, rx, ry, 0.0, math.Pi/2) - - p.LineTo(box.Left+radius, box.Bottom) - - // left bottom - cx = box.Left + radius - cy = box.Bottom - radius - p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2) - - p.LineTo(box.Left, box.Top+radius) - - // left top - cx = box.Left + radius - cy = box.Top + radius - p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2) - - p.Close() - p.FillStroke() - p.Fill() - return p -} - -func (p *Painter) LegendLineDot(box Box) *Painter { - width := box.Width() - height := box.Height() - strokeWidth := 3 - dotHeight := 5 - - p.render.SetStrokeWidth(float64(strokeWidth)) - center := (height-strokeWidth)>>1 - 1 - p.MoveTo(box.Left, box.Top-center) - p.LineTo(box.Right, box.Top-center) - p.Stroke() - p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center) - p.FillStroke() - return p -} - -func (p *Painter) GetRenderer() chart.Renderer { - return p.render -} diff --git a/painter_test.go b/painter_test.go deleted file mode 100644 index 07c4113..0000000 --- a/painter_test.go +++ /dev/null @@ -1,399 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - "testing" - - "github.com/golang/freetype/truetype" - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestPainterOption(t *testing.T) { - assert := assert.New(t) - - font := &truetype.Font{} - d, err := NewPainter(PainterOptions{ - Width: 800, - Height: 600, - Type: ChartOutputSVG, - }, - PainterBoxOption(Box{ - Right: 400, - Bottom: 300, - }), - PainterPaddingOption(Box{ - Left: 1, - Top: 2, - Right: 3, - Bottom: 4, - }), - PainterFontOption(font), - PainterStyleOption(Style{ - ClassName: "test", - }), - ) - assert.Nil(err) - assert.Equal(Box{ - Left: 1, - Top: 2, - Right: 397, - Bottom: 296, - }, d.box) - assert.Equal(font, d.font) - assert.Equal("test", d.style.ClassName) -} - -func TestPainter(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - fn func(*Painter) - result string - }{ - // moveTo, lineTo - { - fn: func(p *Painter) { - p.MoveTo(1, 1) - p.LineTo(2, 2) - p.Stroke() - }, - result: "\\n", - }, - // circle - { - fn: func(p *Painter) { - p.Circle(5, 2, 3) - }, - result: "\\n", - }, - // text - { - fn: func(p *Painter) { - p.Text("hello world!", 3, 6) - }, - result: "\\nhello world!", - }, - // line stroke - { - fn: func(p *Painter) { - p.SetDrawingStyle(Style{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - }) - p.LineStroke([]Point{ - { - X: 1, - Y: 2, - }, - { - X: 3, - Y: 4, - }, - }) - }, - result: "\\n", - }, - // set background - { - fn: func(p *Painter) { - p.SetBackground(400, 300, chart.ColorWhite) - }, - result: "\\n", - }, - // arcTo - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlue, - }) - p.ArcTo(100, 100, 100, 100, 0, math.Pi/2) - p.Close() - p.FillStroke() - }, - result: "\\n", - }, - // pin - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.Pin(30, 30, 30) - }, - result: "\\n", - }, - // arrow left - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.ArrowLeft(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow right - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.ArrowRight(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow top - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.ArrowTop(30, 30, 10, 16) - }, - result: "\\n", - }, - // arrow bottom - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.ArrowBottom(30, 30, 10, 16) - }, - result: "\\n", - }, - // mark line - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - StrokeDashArray: []float64{ - 4, - 2, - }, - }) - p.MarkLine(0, 20, 300) - }, - result: "\\n", - }, - // polygon - { - fn: func(p *Painter) { - p.SetStyle(Style{ - StrokeWidth: 1, - StrokeColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.Polygon(Point{ - X: 100, - Y: 100, - }, 50, 6) - }, - result: "\\n", - }, - // FillArea - { - fn: func(p *Painter) { - p.SetDrawingStyle(Style{ - FillColor: Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - p.FillArea([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 0, - Y: 100, - }, - { - X: 100, - Y: 100, - }, - { - X: 0, - Y: 0, - }, - }) - }, - result: "\\n", - }, - } - for _, tt := range tests { - d, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - Type: ChartOutputSVG, - }, PainterPaddingOption(chart.Box{ - Left: 5, - Top: 10, - })) - assert.Nil(err) - tt.fn(d) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestRoundedRect(t *testing.T) { - assert := assert.New(t) - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - Type: ChartOutputSVG, - }) - assert.Nil(err) - p.OverrideDrawingStyle(Style{ - FillColor: drawing.ColorWhite, - StrokeWidth: 1, - StrokeColor: drawing.ColorWhite, - }).RoundedRect(Box{ - Left: 10, - Right: 30, - Bottom: 150, - Top: 10, - }, 5) - buf, err := p.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(buf)) -} - -func TestPainterTextFit(t *testing.T) { - assert := assert.New(t) - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - Type: ChartOutputSVG, - }) - assert.Nil(err) - f, _ := GetDefaultFont() - style := Style{ - FontSize: 12, - FontColor: chart.ColorBlack, - Font: f, - } - p.SetStyle(style) - box := p.TextFit("Hello World!", 0, 20, 80) - assert.Equal(chart.Box{ - Right: 45, - Bottom: 35, - }, box) - - box = p.TextFit("Hello World!", 0, 100, 200) - assert.Equal(chart.Box{ - Right: 84, - Bottom: 15, - }, box) - - buf, err := p.Bytes() - assert.Nil(err) - assert.Equal(`\nHelloWorld!Hello World!`, string(buf)) -} diff --git a/pie_chart.go b/pie_chart.go deleted file mode 100644 index 5c04ed8..0000000 --- a/pie_chart.go +++ /dev/null @@ -1,318 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "math" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type pieChart struct { - p *Painter - opt *PieChartOption -} - -type PieChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The padding of line chart - Padding Box - // The option of title - Title TitleOption - // The legend option - Legend LegendOption - // background is filled - backgroundIsFilled bool -} - -// NewPieChart returns a pie chart renderer -func NewPieChart(p *Painter, opt PieChartOption) *pieChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &pieChart{ - p: p, - opt: &opt, - } -} - -type sector struct { - value float64 - percent float64 - cx int - cy int - rx float64 - ry float64 - start float64 - delta float64 - offset int - quadrant int - lineStartX int - lineStartY int - lineBranchX int - lineBranchY int - lineEndX int - lineEndY int - showLabel bool - label string - series Series - color Color -} - -func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector { - s := sector{} - s.value = value - s.percent = value / totalValue - s.cx = cx - s.cy = cy - s.rx = radius - s.ry = radius - p := (currentValue + value/2) / totalValue - if p < 0.25 { - s.quadrant = 1 - } else if p < 0.5 { - s.quadrant = 4 - } else if p < 0.75 { - s.quadrant = 3 - } else { - s.quadrant = 2 - } - s.start = chart.PercentToRadians(currentValue/totalValue) - math.Pi/2 - s.delta = chart.PercentToRadians(value / totalValue) - angle := s.start + s.delta/2 - s.lineStartX = cx + int(radius*math.Cos(angle)) - s.lineStartY = cy + int(radius*math.Sin(angle)) - s.lineBranchX = cx + int(labelRadius*math.Cos(angle)) - s.lineBranchY = cy + int(labelRadius*math.Sin(angle)) - s.offset = labelLineLength - if s.lineBranchX <= cx { - s.offset *= -1 - } - s.lineEndX = s.lineBranchX + s.offset - s.lineEndY = s.lineBranchY - s.series = series - s.color = color - s.showLabel = series.Label.Show - s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent) - return s -} - -func (s *sector) calculateY(prevY int) int { - for i := 0; i <= s.cy; i++ { - if s.quadrant <= 2 { - if (prevY - s.lineBranchY) > labelFontSize+5 { - break - } - s.lineBranchY -= 1 - } else { - if (s.lineBranchY - prevY) > labelFontSize+5 { - break - } - s.lineBranchY += 1 - } - } - s.lineEndY = s.lineBranchY - return s.lineBranchY -} - -func (s *sector) calculateTextXY(textBox Box) (x int, y int) { - textMargin := 3 - x = s.lineEndX + textMargin - y = s.lineEndY + textBox.Height()>>1 - 1 - if s.offset < 0 { - textWidth := textBox.Width() - x = s.lineEndX - textWidth - textMargin - } - return -} - -func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - opt := p.opt - values := make([]float64, len(seriesList)) - total := float64(0) - radiusValue := "" - for index, series := range seriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - value := float64(0) - for _, item := range series.Data { - value += item.Value - } - values[index] = value - total += value - } - if total <= 0 { - return BoxZero, errors.New("The sum value of pie chart should gt 0") - } - seriesPainter := result.seriesPainter - cx := seriesPainter.Width() >> 1 - cy := seriesPainter.Height() >> 1 - - diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height()) - radius := getRadius(float64(diameter), radiusValue) - - labelLineWidth := 15 - if radius < 50 { - labelLineWidth = 10 - } - labelRadius := radius + float64(labelLineWidth) - seriesNames := opt.Legend.Data - if len(seriesNames) == 0 { - seriesNames = seriesList.Names() - } - theme := opt.Theme - - currentValue := float64(0) - - var quadrant1, quadrant2, quadrant3, quadrant4 []sector - for index, v := range values { - series := seriesList[index] - color := theme.GetSeriesColor(index) - if index == len(values)-1 { - if color == theme.GetSeriesColor(0) { - color = theme.GetSeriesColor(1) - } - } - s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color) - switch quadrant := s.quadrant; quadrant { - case 1: - quadrant1 = append([]sector{s}, quadrant1...) - case 2: - quadrant2 = append(quadrant2, s) - case 3: - quadrant3 = append([]sector{s}, quadrant3...) - case 4: - quadrant4 = append(quadrant4, s) - } - currentValue += v - } - sectors := append(quadrant1, quadrant4...) - sectors = append(sectors, quadrant3...) - sectors = append(sectors, quadrant2...) - - currentQuadrant := 0 - prevY := 0 - maxY := 0 - minY := 0 - for _, s := range sectors { - seriesPainter.OverrideDrawingStyle(Style{ - StrokeWidth: 1, - StrokeColor: s.color, - FillColor: s.color, - }) - seriesPainter.MoveTo(s.cx, s.cy) - seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke() - if !s.showLabel { - continue - } - if currentQuadrant != s.quadrant { - if s.quadrant == 1 { - minY = cy * 2 - maxY = 0 - prevY = cy * 2 - } - if s.quadrant == 2 { - if currentQuadrant != 3 { - prevY = s.lineEndY - } else { - prevY = minY - } - } - if s.quadrant == 3 { - if currentQuadrant != 4 { - prevY = s.lineEndY - } else { - minY = cy * 2 - maxY = 0 - prevY = 0 - } - } - if s.quadrant == 4 { - if currentQuadrant != 1 { - prevY = s.lineEndY - } else { - prevY = maxY - } - } - currentQuadrant = s.quadrant - } - prevY = s.calculateY(prevY) - if prevY > maxY { - maxY = prevY - } - if prevY < minY { - minY = prevY - } - seriesPainter.MoveTo(s.lineStartX, s.lineStartY) - seriesPainter.LineTo(s.lineBranchX, s.lineBranchY) - seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY) - seriesPainter.LineTo(s.lineEndX, s.lineEndY) - seriesPainter.Stroke() - textStyle := Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !s.series.Label.Color.IsZero() { - textStyle.FontColor = s.series.Label.Color - } - seriesPainter.OverrideTextStyle(textStyle) - x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label)) - seriesPainter.Text(s.label, x, y) - } - return p.p.box, nil -} - -func (p *pieChart) Render() (Box, error) { - opt := p.opt - - renderResult, err := defaultRender(p.p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: XAxisOption{ - Show: FalseFlag(), - }, - YAxisOptions: []YAxisOption{ - { - Show: FalseFlag(), - }, - }, - TitleOption: opt.Title, - LegendOption: opt.Legend, - backgroundIsFilled: opt.backgroundIsFilled, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypePie) - return p.render(renderResult, seriesList) -} diff --git a/pie_chart_test.go b/pie_chart_test.go deleted file mode 100644 index 3795d32..0000000 --- a/pie_chart_test.go +++ /dev/null @@ -1,533 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strconv" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPieChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 1048, - 735, - 580, - 484, - 300, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - }), - Title: TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - Left: PositionCenter, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Orient: OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: PositionLeft, - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nSearch EngineDirectEmailUnion AdsVideo AdsRainfall vs EvaporationFake DataSearch Engine: 33.3%Direct: 23.35%Email: 18.43%Union Ads: 15.37%Video Ads: 9.53%", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartWithLabelsValuesSortedDescending(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 84358845, - 68070697, - 58850717, - 48059777, - 36753736, - 19051562, - 17947406, - 11754004, - 10827529, - 10521556, - 10467366, - 10394055, - 9597085, - 9104772, - 6447710, - 5932654, - 5563970, - 5428792, - 5194336, - 3850894, - 2857279, - 2116792, - 1883008, - 1373101, - 920701, - 660809, - 542051, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "European Union member states by population", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "Germany", - "France", - "Italy", - "Spain", - "Poland", - "Romania", - "Netherlands", - "Belgium", - "Czech Republic", - "Sweden", - "Portugal", - "Greece", - "Hungary", - "Austria", - "Bulgaria", - "Denmark", - "Finland", - "Slovakia", - "Ireland", - "Croatia", - "Lithuania", - "Slovenia", - "Latvia", - "Estonia", - "Cyprus", - "Luxembourg", - "Malta", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEuropean Union member states by populationGermany (84358845 ≅ 18.8%)France (68070697 ≅ 15.17%)Italy (58850717 ≅ 13.12%)Netherlands (17947406 ≅ 4%)Romania (19051562 ≅ 4.24%)Poland (36753736 ≅ 8.19%)Spain (48059777 ≅ 10.71%)Belgium (11754004 ≅ 2.62%)Czech Republic (10827529 ≅ 2.41%)Sweden (10521556 ≅ 2.34%)Portugal (10467366 ≅ 2.33%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Austria (9104772 ≅ 2.02%)Bulgaria (6447710 ≅ 1.43%)Denmark (5932654 ≅ 1.32%)Finland (5563970 ≅ 1.24%)Slovakia (5428792 ≅ 1.21%)Ireland (5194336 ≅ 1.15%)Croatia (3850894 ≅ 0.85%)Lithuania (2857279 ≅ 0.63%)Slovenia (2116792 ≅ 0.47%)Latvia (1883008 ≅ 0.41%)Estonia (1373101 ≅ 0.3%)Cyprus (920701 ≅ 0.2%)Luxembourg (660809 ≅ 0.14%)Malta (542051 ≅ 0.12%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 800, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartWithLabelsValuesUnsorted(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 9104772, - 11754004, - 6447710, - 3850894, - 920701, - 10827529, - 5932654, - 1373101, - 5563970, - 68070697, - 84358845, - 10394055, - 9597085, - 5194336, - 58850717, - 1883008, - 2857279, - 660809, - 542051, - 17947406, - 36753736, - 10467366, - 19051562, - 5428792, - 2116792, - 48059777, - 10521556, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "European Union member states by population", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "Austria", - "Belgium", - "Bulgaria", - "Croatia", - "Cyprus", - "Czech Republic", - "Denmark", - "Estonia", - "Finland", - "France", - "Germany", - "Greece", - "Hungary", - "Ireland", - "Italy", - "Latvia", - "Lithuania", - "Luxembourg", - "Malta", - "Netherlands", - "Poland", - "Portugal", - "Romania", - "Slovakia", - "Slovenia", - "Spain", - "Sweden", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nEuropean Union member states by populationFrance (68070697 ≅ 15.17%)Finland (5563970 ≅ 1.24%)Estonia (1373101 ≅ 0.3%)Denmark (5932654 ≅ 1.32%)Czech Republic (10827529 ≅ 2.41%)Cyprus (920701 ≅ 0.2%)Croatia (3850894 ≅ 0.85%)Bulgaria (6447710 ≅ 1.43%)Belgium (11754004 ≅ 2.62%)Austria (9104772 ≅ 2.02%)Germany (84358845 ≅ 18.8%)Greece (10394055 ≅ 2.31%)Hungary (9597085 ≅ 2.13%)Poland (36753736 ≅ 8.19%)Netherlands (17947406 ≅ 4%)Malta (542051 ≅ 0.12%)Luxembourg (660809 ≅ 0.14%)Lithuania (2857279 ≅ 0.63%)Latvia (1883008 ≅ 0.41%)Italy (58850717 ≅ 13.12%)Ireland (5194336 ≅ 1.15%)Portugal (10467366 ≅ 2.33%)Romania (19051562 ≅ 4.24%)Slovakia (5428792 ≅ 1.21%)Slovenia (2116792 ≅ 0.47%)Spain (48059777 ≅ 10.71%)Sweden (10521556 ≅ 2.34%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 800, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartWith100Labels(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - var values []float64 - var labels []string - for i := 1; i <= 100; i++ { - values = append(values, float64(1)) - labels = append(labels, "Label "+strconv.Itoa(i)) - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - }, - Radius: "200", - }), - Title: TitleOption{ - Text: "Test with 100 labels", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: labels, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nTest with 100 labelsLabel 25: 1%Label 24: 1%Label 23: 1%Label 22: 1%Label 21: 1%Label 20: 1%Label 19: 1%Label 18: 1%Label 17: 1%Label 16: 1%Label 15: 1%Label 14: 1%Label 13: 1%Label 12: 1%Label 11: 1%Label 10: 1%Label 9: 1%Label 8: 1%Label 7: 1%Label 6: 1%Label 5: 1%Label 4: 1%Label 3: 1%Label 2: 1%Label 1: 1%Label 26: 1%Label 27: 1%Label 28: 1%Label 29: 1%Label 30: 1%Label 31: 1%Label 32: 1%Label 33: 1%Label 34: 1%Label 35: 1%Label 36: 1%Label 37: 1%Label 38: 1%Label 39: 1%Label 40: 1%Label 41: 1%Label 42: 1%Label 43: 1%Label 44: 1%Label 45: 1%Label 46: 1%Label 47: 1%Label 48: 1%Label 49: 1%Label 50: 1%Label 75: 1%Label 74: 1%Label 73: 1%Label 72: 1%Label 71: 1%Label 70: 1%Label 69: 1%Label 68: 1%Label 67: 1%Label 66: 1%Label 65: 1%Label 64: 1%Label 63: 1%Label 62: 1%Label 61: 1%Label 60: 1%Label 59: 1%Label 58: 1%Label 57: 1%Label 56: 1%Label 55: 1%Label 54: 1%Label 53: 1%Label 52: 1%Label 51: 1%Label 76: 1%Label 77: 1%Label 78: 1%Label 79: 1%Label 80: 1%Label 81: 1%Label 82: 1%Label 83: 1%Label 84: 1%Label 85: 1%Label 86: 1%Label 87: 1%Label 88: 1%Label 89: 1%Label 90: 1%Label 91: 1%Label 92: 1%Label 93: 1%Label 94: 1%Label 95: 1%Label 96: 1%Label 97: 1%Label 98: 1%Label 99: 1%Label 100: 1%", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1000, - Height: 900, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestPieChartFixLabelPos72586(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := []float64{ - 397594, - 185596, - 149086, - 144258, - 120194, - 117514, - 99412, - 91135, - 87282, - 76790, - 72586, - 58818, - 58270, - 56306, - 55486, - 54792, - 53746, - 51460, - 41242, - 39476, - 37414, - 36644, - 33784, - 32788, - 32566, - 29608, - 29558, - 29384, - 28166, - 26998, - 26948, - 26054, - 25804, - 25730, - 24438, - 23782, - 22896, - 21404, - 428978, - } - _, err := NewPieChart(p, PieChartOption{ - SeriesList: NewPieSeriesList(values, PieSeriesOption{ - Label: SeriesLabel{ - Show: true, - Formatter: "{b} ({c} ≅ {d})", - }, - Radius: "150", - }), - Title: TitleOption{ - Text: "Fix label K (72586)", - Left: PositionRight, - }, - Padding: Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - Legend: LegendOption{ - Data: []string{ - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", - "I", - "J", - "K", - "L", - "M", - "N", - "O", - "P", - "Q", - "R", - "S", - "T", - "U", - "V", - "W", - "X", - "Y", - "Z", - "AA", - "AB", - "AC", - "AD", - "AE", - "AF", - "AG", - "AH", - "AI", - "AJ", - "AK", - "AL", - "AM", - }, - Show: FalseFlag(), - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nFix label K (72586)C (149086 ≅ 5.04%)B (185596 ≅ 6.28%)A (397594 ≅ 13.45%)D (144258 ≅ 4.88%)E (120194 ≅ 4.06%)F (117514 ≅ 3.97%)G (99412 ≅ 3.36%)H (91135 ≅ 3.08%)I (87282 ≅ 2.95%)J (76790 ≅ 2.59%)Z (29608 ≅ 1%)Y (32566 ≅ 1.1%)X (32788 ≅ 1.1%)W (33784 ≅ 1.14%)V (36644 ≅ 1.24%)U (37414 ≅ 1.26%)T (39476 ≅ 1.33%)S (41242 ≅ 1.39%)R (51460 ≅ 1.74%)Q (53746 ≅ 1.81%)P (54792 ≅ 1.85%)O (55486 ≅ 1.87%)N (56306 ≅ 1.9%)M (58270 ≅ 1.97%)L (58818 ≅ 1.99%)K (72586 ≅ 2.45%)AA (29558 ≅ 1%)AB (29384 ≅ 0.99%)AC (28166 ≅ 0.95%)AD (26998 ≅ 0.91%)AE (26948 ≅ 0.91%)AF (26054 ≅ 0.88%)AG (25804 ≅ 0.87%)AH (25730 ≅ 0.87%)AI (24438 ≅ 0.82%)AJ (23782 ≅ 0.8%)AK (22896 ≅ 0.77%)AL (21404 ≅ 0.72%)AM (428978 ≅ 14.52%)", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 1150, - Height: 550, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/radar_chart.go b/radar_chart.go deleted file mode 100644 index cf18135..0000000 --- a/radar_chart.go +++ /dev/null @@ -1,273 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - - "github.com/dustin/go-humanize" - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -type radarChart struct { - p *Painter - opt *RadarChartOption -} - -type RadarIndicator struct { - // Indicator's name - Name string - // The maximum value of indicator - Max float64 - // The minimum value of indicator - Min float64 -} - -type RadarChartOption struct { - // The theme - Theme ColorPalette - // The font size - Font *truetype.Font - // The data series list - SeriesList SeriesList - // The padding of line chart - Padding Box - // The option of title - Title TitleOption - // The legend option - Legend LegendOption - // The radar indicator list - RadarIndicators []RadarIndicator - // background is filled - backgroundIsFilled bool -} - -// NewRadarIndicators returns a radar indicator list -func NewRadarIndicators(names []string, values []float64) []RadarIndicator { - if len(names) != len(values) { - return nil - } - indicators := make([]RadarIndicator, len(names)) - for index, name := range names { - indicators[index] = RadarIndicator{ - Name: name, - Max: values[index], - } - } - return indicators -} - -// NewRadarChart returns a radar chart renderer -func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &radarChart{ - p: p, - opt: &opt, - } -} - -func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { - opt := r.opt - indicators := opt.RadarIndicators - sides := len(indicators) - if sides < 3 { - return BoxZero, errors.New("The count of indicator should be >= 3") - } - maxValues := make([]float64, len(indicators)) - for _, series := range seriesList { - for index, item := range series.Data { - if index < len(maxValues) && item.Value > maxValues[index] { - maxValues[index] = item.Value - } - } - } - for index, indicator := range indicators { - if indicator.Max <= 0 { - indicators[index].Max = maxValues[index] - } - } - - radiusValue := "" - for _, series := range seriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - } - - seriesPainter := result.seriesPainter - theme := opt.Theme - - cx := seriesPainter.Width() >> 1 - cy := seriesPainter.Height() >> 1 - diameter := chart.MinInt(seriesPainter.Width(), seriesPainter.Height()) - radius := getRadius(float64(diameter), radiusValue) - - divideCount := 5 - divideRadius := float64(int(radius / float64(divideCount))) - radius = divideRadius * float64(divideCount) - - seriesPainter.OverrideDrawingStyle(Style{ - StrokeColor: theme.GetAxisSplitLineColor(), - StrokeWidth: 1, - }) - center := Point{ - X: cx, - Y: cy, - } - for i := 0; i < divideCount; i++ { - seriesPainter.Polygon(center, divideRadius*float64(i+1), sides) - } - points := getPolygonPoints(center, radius, sides) - for _, p := range points { - seriesPainter.MoveTo(center.X, center.Y) - seriesPainter.LineTo(p.X, p.Y) - seriesPainter.Stroke() - } - seriesPainter.OverrideTextStyle(Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - }) - offset := 5 - // 文本生成 - for index, p := range points { - name := indicators[index].Name - b := seriesPainter.MeasureText(name) - isXCenter := p.X == center.X - isYCenter := p.Y == center.Y - isRight := p.X > center.X - isLeft := p.X < center.X - isTop := p.Y < center.Y - isBottom := p.Y > center.Y - x := p.X - y := p.Y - if isXCenter { - x -= b.Width() >> 1 - if isTop { - y -= b.Height() - } else { - y += b.Height() - } - } - if isYCenter { - y += b.Height() >> 1 - } - if isTop { - y += offset - } - if isBottom { - y += offset - } - if isRight { - x += offset - } - if isLeft { - x -= (b.Width() + offset) - } - seriesPainter.Text(name, x, y) - } - - // 雷达图 - angles := getPolygonPointAngles(sides) - maxCount := len(indicators) - for _, series := range seriesList { - linePoints := make([]Point, 0, maxCount) - for j, item := range series.Data { - if j >= maxCount { - continue - } - indicator := indicators[j] - var percent float64 - offset := indicator.Max - indicator.Min - if offset > 0 { - percent = (item.Value - indicator.Min) / offset - } - r := percent * radius - p := getPolygonPoint(center, r, angles[j]) - linePoints = append(linePoints, p) - } - color := theme.GetSeriesColor(series.index) - dotFillColor := drawing.ColorWhite - if theme.IsDark() { - dotFillColor = color - } - linePoints = append(linePoints, linePoints[0]) - seriesPainter.OverrideDrawingStyle(Style{ - StrokeColor: color, - StrokeWidth: defaultStrokeWidth, - DotWidth: defaultDotWidth, - DotColor: color, - FillColor: color.WithAlpha(20), - }) - seriesPainter.LineStroke(linePoints). - FillArea(linePoints) - dotWith := 2.0 - seriesPainter.OverrideDrawingStyle(Style{ - StrokeWidth: defaultStrokeWidth, - StrokeColor: color, - FillColor: dotFillColor, - }) - for index, point := range linePoints { - seriesPainter.Circle(dotWith, point.X, point.Y) - seriesPainter.FillStroke() - if series.Label.Show && index < len(series.Data) { - value := humanize.FtoaWithDigits(series.Data[index].Value, 2) - b := seriesPainter.MeasureText(value) - seriesPainter.Text(value, point.X-b.Width()/2, point.Y) - } - - } - } - - return r.p.box, nil -} - -func (r *radarChart) Render() (Box, error) { - p := r.p - opt := r.opt - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: opt.SeriesList, - XAxis: XAxisOption{ - Show: FalseFlag(), - }, - YAxisOptions: []YAxisOption{ - { - Show: FalseFlag(), - }, - }, - TitleOption: opt.Title, - LegendOption: opt.Legend, - backgroundIsFilled: opt.backgroundIsFilled, - }) - if err != nil { - return BoxZero, err - } - seriesList := opt.SeriesList.Filter(ChartTypeRadar) - return r.render(renderResult, seriesList) -} diff --git a/radar_chart_test.go b/radar_chart_test.go deleted file mode 100644 index 79fd9ac..0000000 --- a/radar_chart_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRadarChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - values := [][]float64{ - { - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }, - { - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }, - } - _, err := NewRadarChart(p, RadarChartOption{ - SeriesList: NewSeriesListDataFromValues(values, ChartTypeRadar), - Title: TitleOption{ - Text: "Basic Radar Chart", - }, - Legend: NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - RadarIndicators: NewRadarIndicators([]string{ - "Sales", - "Administration", - "Information Technology", - "Customer Support", - "Development", - "Marketing", - }, []float64{ - 6500, - 16000, - 30000, - 38000, - 52000, - 25000, - }), - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nAllocated BudgetActual SpendingBasic Radar ChartSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p.Child(PainterPaddingOption(Box{ - Left: 20, - Top: 20, - Right: 20, - Bottom: 20, - }))) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/range.go b/range.go deleted file mode 100644 index ec64c2d..0000000 --- a/range.go +++ /dev/null @@ -1,144 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" -) - -const defaultAxisDivideCount = 6 - -type axisRange struct { - p *Painter - divideCount int - min float64 - max float64 - size int - boundary bool -} - -type AxisRangeOption struct { - Painter *Painter - // The min value of axis - Min float64 - // The max value of axis - Max float64 - // The size of axis - Size int - // Boundary gap - Boundary bool - // The count of divide - DivideCount int -} - -// NewRange returns a axis range -func NewRange(opt AxisRangeOption) axisRange { - max := opt.Max - min := opt.Min - - max += math.Abs(max * 0.1) - min -= math.Abs(min * 0.1) - divideCount := opt.DivideCount - r := math.Abs(max - min) - - // 最小单位计算 - unit := 1 - if r > 5 { - unit = 2 - } - if r > 10 { - unit = 4 - } - if r > 30 { - unit = 5 - } - if r > 100 { - unit = 10 - } - if r > 200 { - unit = 20 - } - unit = int((r/float64(divideCount))/float64(unit))*unit + unit - - if min != 0 { - isLessThanZero := min < 0 - min = float64(int(min/float64(unit)) * unit) - // 如果是小于0,int的时候向上取整了,因此调整 - if min < 0 || - (isLessThanZero && min == 0) { - min -= float64(unit) - } - } - max = min + float64(unit*divideCount) - expectMax := opt.Max * 2 - if max > expectMax { - max = float64(ceilFloatToInt(expectMax)) - } - return axisRange{ - p: opt.Painter, - divideCount: divideCount, - min: min, - max: max, - size: opt.Size, - boundary: opt.Boundary, - } -} - -// Values returns values of range -func (r axisRange) Values() []string { - offset := (r.max - r.min) / float64(r.divideCount) - values := make([]string, 0) - formatter := commafWithDigits - if r.p != nil && r.p.valueFormatter != nil { - formatter = r.p.valueFormatter - } - for i := 0; i <= r.divideCount; i++ { - v := r.min + float64(i)*offset - value := formatter(v) - values = append(values, value) - } - return values -} - -func (r *axisRange) getHeight(value float64) int { - if r.max <= r.min { - return 0 - } - v := (value - r.min) / (r.max - r.min) - return int(v * float64(r.size)) -} - -func (r *axisRange) getRestHeight(value float64) int { - return r.size - r.getHeight(value) -} - -// GetRange returns a range of index -func (r *axisRange) GetRange(index int) (float64, float64) { - unit := float64(r.size) / float64(r.divideCount) - return unit * float64(index), unit * float64(index+1) -} - -// AutoDivide divides the axis -func (r *axisRange) AutoDivide() []int { - return autoDivide(r.size, r.divideCount) -} diff --git a/series.go b/series.go deleted file mode 100644 index da50e64..0000000 --- a/series.go +++ /dev/null @@ -1,318 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -package charts - -import ( - "math" - "strings" - - "github.com/dustin/go-humanize" - "git.smarteching.com/zeni/go-chart/v2" -) - -type SeriesData struct { - // The value of series data - Value float64 - // The style of series data - Style Style -} - -// NewSeriesListDataFromValues returns a series list -func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, chartType...) - } - return seriesList -} - -// NewSeriesFromValues returns a series -func NewSeriesFromValues(values []float64, chartType ...string) Series { - s := Series{ - Data: NewSeriesDataFromValues(values), - } - if len(chartType) != 0 { - s.Type = chartType[0] - } - return s -} - -// NewSeriesDataFromValues return a series data -func NewSeriesDataFromValues(values []float64) []SeriesData { - data := make([]SeriesData, len(values)) - for index, value := range values { - data[index] = SeriesData{ - Value: value, - } - } - return data -} - -type SeriesLabel struct { - // Data label formatter, which supports string template. - // {b}: the name of a data item. - // {c}: the value of a data item. - // {d}: the percent of a data item(pie chart). - Formatter string - // The color for label - Color Color - // Show flag for label - Show bool - // Distance to the host graphic element. - Distance int - // The position of label - Position string - // The offset of label's position - Offset Box - // The font size of label - FontSize float64 -} - -const ( - SeriesMarkDataTypeMax = "max" - SeriesMarkDataTypeMin = "min" - SeriesMarkDataTypeAverage = "average" -) - -type SeriesMarkData struct { - // The mark data type, it can be "max", "min", "average". - // The "average" is only for mark line - Type string -} -type SeriesMarkPoint struct { - // The width of symbol, default value is 30 - SymbolSize int - // The mark data of series mark point - Data []SeriesMarkData -} -type SeriesMarkLine struct { - // The mark data of series mark line - Data []SeriesMarkData -} -type Series struct { - index int - // The type of series, it can be "line", "bar" or "pie". - // Default value is "line" - Type string - // The data list of series - Data []SeriesData - // The Y axis index, it should be 0 or 1. - // Default value is 0 - AxisIndex int - // The style for series - Style chart.Style - // The label for series - Label SeriesLabel - // The name of series - Name string - // Radius for Pie chart, e.g.: 40%, default is "40%" - Radius string - // Round for bar chart - RoundRadius int - // Mark point for series - MarkPoint SeriesMarkPoint - // Make line for series - MarkLine SeriesMarkLine - // Max value of series - Min *float64 - // Min value of series - Max *float64 -} -type SeriesList []Series - -func (sl SeriesList) init() { - if len(sl) == 0 { - return - } - if sl[len(sl)-1].index != 0 { - return - } - for i := 0; i < len(sl); i++ { - if sl[i].Type == "" { - sl[i].Type = ChartTypeLine - } - sl[i].index = i - } -} - -func (sl SeriesList) Filter(chartType string) SeriesList { - arr := make(SeriesList, 0) - for index, item := range sl { - if item.Type == chartType { - arr = append(arr, sl[index]) - } - } - return arr -} - -// GetMaxMin get max and min value of series list -func (sl SeriesList) GetMaxMin(axisIndex int) (float64, float64) { - min := math.MaxFloat64 - max := -math.MaxFloat64 - for _, series := range sl { - if series.AxisIndex != axisIndex { - continue - } - for _, item := range series.Data { - // 如果为空值,忽略 - if item.Value == nullValue { - continue - } - if item.Value > max { - max = item.Value - } - if item.Value < min { - min = item.Value - } - } - } - return max, min -} - -type PieSeriesOption struct { - Radius string - Label SeriesLabel - Names []string -} - -func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { - result := make([]Series, len(values)) - var opt PieSeriesOption - if len(opts) != 0 { - opt = opts[0] - } - for index, v := range values { - name := "" - if index < len(opt.Names) { - name = opt.Names[index] - } - s := Series{ - Type: ChartTypePie, - Data: []SeriesData{ - { - Value: v, - }, - }, - Radius: opt.Radius, - Label: opt.Label, - Name: name, - } - result[index] = s - } - return result -} - -type seriesSummary struct { - // The index of max value - MaxIndex int - // The max value - MaxValue float64 - // The index of min value - MinIndex int - // The min value - MinValue float64 - // THe average value - AverageValue float64 -} - -// Summary get summary of series -func (s *Series) Summary() seriesSummary { - minIndex := -1 - maxIndex := -1 - minValue := math.MaxFloat64 - maxValue := -math.MaxFloat64 - sum := float64(0) - for j, item := range s.Data { - if item.Value < minValue { - minIndex = j - minValue = item.Value - } - if item.Value > maxValue { - maxIndex = j - maxValue = item.Value - } - sum += item.Value - } - return seriesSummary{ - MaxIndex: maxIndex, - MaxValue: maxValue, - MinIndex: minIndex, - MinValue: minValue, - AverageValue: sum / float64(len(s.Data)), - } -} - -// Names returns the names of series list -func (sl SeriesList) Names() []string { - names := make([]string, len(sl)) - for index, s := range sl { - names[index] = s.Name - } - return names -} - -// LabelFormatter label formatter -type LabelFormatter func(index int, value float64, percent float64) string - -// NewPieLabelFormatter returns a pie label formatter -func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{b}: {d}" - } - return NewLabelFormatter(seriesNames, layout) -} - -// NewFunnelLabelFormatter returns a funner label formatter -func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{b}({d})" - } - return NewLabelFormatter(seriesNames, layout) -} - -// NewValueLabelFormatter returns a value formatter -func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{c}" - } - return NewLabelFormatter(seriesNames, layout) -} - -// NewLabelFormatter returns a label formaatter -func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { - return func(index int, value, percent float64) string { - // 如果无percent的则设置为<0 - percentText := "" - if percent >= 0 { - percentText = humanize.FtoaWithDigits(percent*100, 2) + "%" - } - valueText := humanize.FtoaWithDigits(value, 2) - name := "" - if len(seriesNames) > index { - name = seriesNames[index] - } - text := strings.ReplaceAll(layout, "{c}", valueText) - text = strings.ReplaceAll(text, "{d}", percentText) - text = strings.ReplaceAll(text, "{b}", name) - return text - } -} diff --git a/series_label.go b/series_label.go deleted file mode 100644 index af873fc..0000000 --- a/series_label.go +++ /dev/null @@ -1,148 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" -) - -type labelRenderValue struct { - Text string - Style Style - X int - Y int - // 旋转 - Radians float64 -} - -type LabelValue struct { - Index int - Value float64 - X int - Y int - // 旋转 - Radians float64 - // 字体颜色 - FontColor Color - // 字体大小 - FontSize float64 - Orient string - Offset Box -} - -type SeriesLabelPainter struct { - p *Painter - seriesNames []string - label *SeriesLabel - theme ColorPalette - font *truetype.Font - values []labelRenderValue -} - -type SeriesLabelPainterParams struct { - P *Painter - SeriesNames []string - Label SeriesLabel - Theme ColorPalette - Font *truetype.Font -} - -func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter { - return &SeriesLabelPainter{ - p: params.P, - seriesNames: params.SeriesNames, - label: ¶ms.Label, - theme: params.Theme, - font: params.Font, - values: make([]labelRenderValue, 0), - } -} - -func (o *SeriesLabelPainter) Add(value LabelValue) { - label := o.label - distance := label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1) - labelStyle := Style{ - FontColor: o.theme.GetTextColor(), - FontSize: labelFontSize, - Font: o.font, - } - if value.FontSize != 0 { - labelStyle.FontSize = value.FontSize - } - if !value.FontColor.IsZero() { - label.Color = value.FontColor - } - if !label.Color.IsZero() { - labelStyle.FontColor = label.Color - } - p := o.p - p.OverrideDrawingStyle(labelStyle) - rotated := value.Radians != 0 - if rotated { - p.SetTextRotation(value.Radians) - } - textBox := p.MeasureText(text) - renderValue := labelRenderValue{ - Text: text, - Style: labelStyle, - X: value.X, - Y: value.Y, - Radians: value.Radians, - } - if value.Orient != OrientHorizontal { - renderValue.X -= textBox.Width() >> 1 - renderValue.Y -= distance - } else { - renderValue.X += distance - renderValue.Y += textBox.Height() >> 1 - renderValue.Y -= 2 - } - if rotated { - renderValue.X = value.X + textBox.Width()>>1 - 1 - p.ClearTextRotation() - } else { - if textBox.Width()%2 != 0 { - renderValue.X++ - } - } - renderValue.X += value.Offset.Left - renderValue.Y += value.Offset.Top - o.values = append(o.values, renderValue) -} - -func (o *SeriesLabelPainter) Render() (Box, error) { - for _, item := range o.values { - o.p.OverrideTextStyle(item.Style) - if item.Radians != 0 { - o.p.TextRotation(item.Text, item.X, item.Y, item.Radians) - } else { - o.p.Text(item.Text, item.X, item.Y) - } - } - return chart.BoxZero, nil -} diff --git a/series_test.go b/series_test.go deleted file mode 100644 index 40d2f91..0000000 --- a/series_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewSeriesListDataFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal(SeriesList{ - { - Type: ChartTypeBar, - Data: []SeriesData{ - { - Value: 1.0, - }, - }, - }, - }, NewSeriesListDataFromValues([][]float64{ - { - 1, - }, - }, ChartTypeBar)) -} - -func TestSeriesLists(t *testing.T) { - assert := assert.New(t) - seriesList := NewSeriesListDataFromValues([][]float64{ - { - 1, - 2, - }, - { - 10, - }, - }, ChartTypeBar) - - assert.Equal(2, len(seriesList.Filter(ChartTypeBar))) - assert.Equal(0, len(seriesList.Filter(ChartTypeLine))) - - max, min := seriesList.GetMaxMin(0) - assert.Equal(float64(10), max) - assert.Equal(float64(1), min) - - assert.Equal(seriesSummary{ - MaxIndex: 1, - MaxValue: 2, - MinIndex: 0, - MinValue: 1, - AverageValue: 1.5, - }, seriesList[0].Summary()) -} - -func TestFormatter(t *testing.T) { - assert := assert.New(t) - - assert.Equal("a: 12%", NewPieLabelFormatter([]string{ - "a", - "b", - }, "")(0, 10, 0.12)) - - assert.Equal("10", NewValueLabelFormatter([]string{ - "a", - "b", - }, "")(0, 10, 0.12)) -} diff --git a/start_zh.md b/start_zh.md deleted file mode 100644 index ee8359c..0000000 --- a/start_zh.md +++ /dev/null @@ -1,254 +0,0 @@ -# go-charts - -`go-charts`主要分为了下几个模块: - -- `标题`:图表的标题,包括主副标题,位置为图表的顶部 -- `图例`:图表的图例列表,用于标识每个图例对应的颜色与名称信息,默认为图表的顶部,可自定义位置 -- `X轴`:图表的x轴,用于折线图、柱状图中,表示每个点对应的时间,位置图表的底部 -- `Y轴`:图表的y轴,用于折线图、柱状图中,最多可使用两组y轴(一左一右),默认位置图表的左侧 -- `内容`: 图表的内容,折线图、柱状图、饼图等,在图表的中间区域 - -## 标题 - -### 常用设置 - -标题一般仅需要设置主副标题即可,其它的属性均会设置默认值,常用的方式是使用`TitleTextOptionFunc`设置,其中副标题为可选值,方式如下: - -```go - charts.TitleTextOptionFunc("Text", "Subtext"), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.Title = charts.TitleOption{ - // 主标题 - Text: "Text", - // 副标题 - Subtext: "Subtext", - // 标题左侧位置,可设置为"center","right",数值("20")或百份比("20%") - Left: charts.PositionRight, - // 标题顶部位置,只可调为数值 - Top: "20", - // 主标题文字大小 - FontSize: 14, - // 副标题文字大小 - SubtextFontSize: 12, - // 主标题字体颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - // 副标题字体影响 - SubtextFontColor: charts.Color{ - R: 200, - G: 200, - B: 200, - A: 255, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.TitleTextOptionFunc("Text", "Subtext"), -func(opt *charts.ChartOption) { - // 修改top的值 - opt.Title.Top = "20" -}, -``` - -## 图例 - -### 常用设置 - -图例组件与图表中的数据一一对应,常用仅设置其名称及左侧的值即可(可选),方式如下: - - -```go -charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", -}, "50"), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.Legend = charts.LegendOption{ - // 图例名称 - Data: []string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }, - // 图例左侧位置,可设置为"center","right",数值("20")或百份比("20%") - // 如果示例有多行,只影响第一行,而且对于多行的示例,设置"center", "right"无效 - Left: "50", - // 图例顶部位置,只可调为数值 - Top: "10", - // 图例图标的位置,默认为左侧,只允许左或右 - Align: charts.AlignRight, - // 图例排列方式,默认为水平,只允许水平或垂直 - Orient: charts.OrientVertical, - // 图标类型,提供"rect"与"lineDot"两种类型 - Icon: charts.IconRect, - // 字体大小 - FontSize: 14, - // 字体颜色 - FontColor: charts.Color{ - R: 150, - G: 150, - B: 150, - A: 255, - }, - // 是否展示,如果不需要展示则设置 - // Show: charts.FalseFlag(), - // 图例区域的padding值 - Padding: charts.Box{ - Top: 10, - Left: 10, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.LegendLabelsOptionFunc([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", -}, "50"), -func(opt *charts.ChartOption) { - opt.Legend.Top = "10" -}, -``` - -## X轴 - -### 常用设置 - -图表中X轴的展示,常用的设置方式是指定数组即可: - -```go -charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", -}), -``` - -### 个性化设置 - -```go -func(opt *charts.ChartOption) { - opt.XAxis = charts.XAxisOption{ - // X轴内容 - Data: []string{ - "01", - "02", - "03", - "04", - "05", - "06", - "07", - "08", - "09", - }, - // 如果数据点不居中,则设置为false - BoundaryGap: charts.FalseFlag(), - // 字体大小 - FontSize: 14, - // 是否展示,如果不需要展示则设置 - // Show: charts.FalseFlag(), - // 会根据文本内容以及此值选择适合的分块大小,一般不需要设置 - // SplitNumber: 3, - // 线条颜色 - StrokeColor: charts.Color{ - R: 200, - G: 200, - B: 200, - A: 255, - }, - // 文字颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - } -}, -``` - -### 部分属性个性化设置 - -```go -charts.XAxisDataOptionFunc([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", -}), -func(opt *charts.ChartOption) { - opt.XAxis.FontColor = charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, -}, -``` - -## Y轴 - -图表中的y轴展示的相关数据会根据图表中的数据自动生成适合的值,如果需要自定义,则可自定义以下部分数据: - -```go -func(opt *charts.ChartOption) { - opt.YAxisOptions = []charts.YAxisOption{ - { - // 字体大小 - FontSize: 16, - // 字体颜色 - FontColor: charts.Color{ - R: 100, - G: 100, - B: 100, - A: 255, - }, - // 内容,{value}会替换为对应的值 - Formatter: "{value} ml", - // Y轴颜色,如果设置此值,会覆盖font color - Color: charts.Color{ - R: 255, - G: 0, - B: 0, - A: 255, - }, - }, - } -}, -``` diff --git a/table.go b/table.go deleted file mode 100644 index 3e6f273..0000000 --- a/table.go +++ /dev/null @@ -1,438 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -type tableChart struct { - p *Painter - opt *TableChartOption -} - -// NewTableChart returns a table chart render -func NewTableChart(p *Painter, opt TableChartOption) *tableChart { - if opt.Theme == nil { - opt.Theme = defaultTheme - } - return &tableChart{ - p: p, - opt: &opt, - } -} - -type TableCell struct { - // Text the text of table cell - Text string - // Style the current style of table cell - Style Style - // Row the row index of table cell - Row int - // Column the column index of table cell - Column int -} - -type TableChartOption struct { - // The output type - Type string - // The width of table - Width int - // The theme - Theme ColorPalette - // The padding of table cell - Padding Box - // The header data of table - Header []string - // The data of table - Data [][]string - // The span list of table column - Spans []int - // The text align list of table cell - TextAligns []string - // The font size of table - FontSize float64 - // The font family, which should be installed first - FontFamily string - Font *truetype.Font - // The font color of table - FontColor Color - // The background color of header - HeaderBackgroundColor Color - // The header font color - HeaderFontColor Color - // The background color of row - RowBackgroundColors []Color - // The background color - BackgroundColor Color - // CellTextStyle customize text style of table cell - CellTextStyle func(TableCell) *Style - // CellStyle customize drawing style of table cell - CellStyle func(TableCell) *Style -} - -type TableSetting struct { - // The color of header - HeaderColor Color - // The color of heder text - HeaderFontColor Color - // The color of table text - FontColor Color - // The color list of row - RowColors []Color - // The padding of cell - Padding Box -} - -var TableLightThemeSetting = TableSetting{ - HeaderColor: Color{ - R: 240, - G: 240, - B: 240, - A: 255, - }, - HeaderFontColor: Color{ - R: 98, - G: 105, - B: 118, - A: 255, - }, - FontColor: Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - RowColors: []Color{ - drawing.ColorWhite, - { - R: 247, - G: 247, - B: 247, - A: 255, - }, - }, - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, -} - -var TableDarkThemeSetting = TableSetting{ - HeaderColor: Color{ - R: 38, - G: 38, - B: 42, - A: 255, - }, - HeaderFontColor: Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - FontColor: Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - RowColors: []Color{ - { - R: 24, - G: 24, - B: 28, - A: 255, - }, - { - R: 38, - G: 38, - B: 42, - A: 255, - }, - }, - Padding: Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, -} - -var tableDefaultSetting = TableLightThemeSetting - -// SetDefaultTableSetting sets the default setting for table -func SetDefaultTableSetting(setting TableSetting) { - tableDefaultSetting = setting -} - -type renderInfo struct { - Width int - Height int - HeaderHeight int - RowHeights []int - ColumnWidths []int -} - -func (t *tableChart) render() (*renderInfo, error) { - info := renderInfo{ - RowHeights: make([]int, 0), - } - p := t.p - opt := t.opt - if len(opt.Header) == 0 { - return nil, errors.New("header can not be nil") - } - theme := opt.Theme - if theme == nil { - theme = p.theme - } - fontSize := opt.FontSize - if fontSize == 0 { - fontSize = 12 - } - fontColor := opt.FontColor - if fontColor.IsZero() { - fontColor = tableDefaultSetting.FontColor - } - font := opt.Font - if font == nil { - font = theme.GetFont() - } - headerFontColor := opt.HeaderFontColor - if opt.HeaderFontColor.IsZero() { - headerFontColor = tableDefaultSetting.HeaderFontColor - } - - spans := opt.Spans - if len(spans) != len(opt.Header) { - newSpans := make([]int, len(opt.Header)) - for index := range opt.Header { - if index >= len(spans) { - newSpans[index] = 1 - } else { - newSpans[index] = spans[index] - } - } - spans = newSpans - } - - sum := sumInt(spans) - values := autoDivideSpans(p.Width(), sum, spans) - columnWidths := make([]int, 0) - for index, v := range values { - if index == len(values)-1 { - break - } - columnWidths = append(columnWidths, values[index+1]-v) - } - info.ColumnWidths = columnWidths - - height := 0 - textStyle := Style{ - FontSize: fontSize, - FontColor: headerFontColor, - FillColor: headerFontColor, - Font: font, - } - - headerHeight := 0 - padding := opt.Padding - if padding.IsZero() { - padding = tableDefaultSetting.Padding - } - getCellTextStyle := opt.CellTextStyle - if getCellTextStyle == nil { - getCellTextStyle = func(_ TableCell) *Style { - return nil - } - } - // textAligns := opt.TextAligns - getTextAlign := func(index int) string { - if len(opt.TextAligns) <= index { - return "" - } - return opt.TextAligns[index] - } - - // 表格单元的处理 - renderTableCells := func( - currentStyle Style, - rowIndex int, - textList []string, - currentHeight int, - cellPadding Box, - ) int { - cellMaxHeight := 0 - paddingHeight := cellPadding.Top + cellPadding.Bottom - paddingWidth := cellPadding.Left + cellPadding.Right - for index, text := range textList { - cellStyle := getCellTextStyle(TableCell{ - Text: text, - Row: rowIndex, - Column: index, - Style: currentStyle, - }) - if cellStyle == nil { - cellStyle = ¤tStyle - } - p.SetStyle(*cellStyle) - x := values[index] - y := currentHeight + cellPadding.Top - width := values[index+1] - x - x += cellPadding.Left - width -= paddingWidth - box := p.TextFit(text, x, y+int(fontSize), width, getTextAlign(index)) - // 计算最高的高度 - if box.Height()+paddingHeight > cellMaxHeight { - cellMaxHeight = box.Height() + paddingHeight - } - } - return cellMaxHeight - } - - // 表头的处理 - headerHeight = renderTableCells(textStyle, 0, opt.Header, height, padding) - height += headerHeight - info.HeaderHeight = headerHeight - - // 表格内容的处理 - textStyle.FontColor = fontColor - textStyle.FillColor = fontColor - for index, textList := range opt.Data { - cellHeight := renderTableCells(textStyle, index+1, textList, height, padding) - info.RowHeights = append(info.RowHeights, cellHeight) - height += cellHeight - } - - info.Width = p.Width() - info.Height = height - return &info, nil -} - -func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { - p := t.p - opt := t.opt - if !opt.BackgroundColor.IsZero() { - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) - } - headerBGColor := opt.HeaderBackgroundColor - if headerBGColor.IsZero() { - headerBGColor = tableDefaultSetting.HeaderColor - } - - // 如果设置表头背景色 - p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) - currentHeight := info.HeaderHeight - rowColors := opt.RowBackgroundColors - if rowColors == nil { - rowColors = tableDefaultSetting.RowColors - } - for index, h := range info.RowHeights { - color := rowColors[index%len(rowColors)] - child := p.Child(PainterPaddingOption(Box{ - Top: currentHeight, - })) - child.SetBackground(p.Width(), h, color, true) - currentHeight += h - } - // 根据是否有设置表格样式调整背景色 - getCellStyle := opt.CellStyle - if getCellStyle != nil { - arr := [][]string{ - opt.Header, - } - arr = append(arr, opt.Data...) - top := 0 - heights := []int{ - info.HeaderHeight, - } - heights = append(heights, info.RowHeights...) - // 循环所有表格单元,生成背景色 - for i, textList := range arr { - left := 0 - for j, v := range textList { - style := getCellStyle(TableCell{ - Text: v, - Row: i, - Column: j, - }) - if style != nil && !style.FillColor.IsZero() { - padding := style.Padding - child := p.Child(PainterPaddingOption(Box{ - Top: top + padding.Top, - Left: left + padding.Left, - })) - w := info.ColumnWidths[j] - padding.Left - padding.Top - h := heights[i] - padding.Top - padding.Bottom - child.SetBackground(w, h, style.FillColor, true) - } - left += info.ColumnWidths[j] - } - top += heights[i] - } - } - _, err := t.render() - if err != nil { - return BoxZero, err - } - - return Box{ - Right: info.Width, - Bottom: info.Height, - }, nil -} - -func (t *tableChart) Render() (Box, error) { - p := t.p - opt := t.opt - if !opt.BackgroundColor.IsZero() { - p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) - } - if opt.Font == nil && opt.FontFamily != "" { - opt.Font, _ = GetFont(opt.FontFamily) - } - - r := p.render - fn := chart.PNG - if p.outputType == ChartOutputSVG { - fn = chart.SVG - } - newRender, err := fn(p.Width(), 100) - if err != nil { - return BoxZero, err - } - p.render = newRender - info, err := t.render() - if err != nil { - return BoxZero, err - } - p.render = r - return t.renderWithInfo(info) -} diff --git a/table_test.go b/table_test.go deleted file mode 100644 index a958c95..0000000 --- a/table_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTableChart(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTableChart(p, TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Spans: []int{ - 1, - 1, - 2, - 1, - // span和header不匹配,最后自动设置为1 - // 1, - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nNameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTableChart(p, TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", - }, - Data: [][]string{ - { - "John Brown", - "32", - "New York No. 1 Lake Park", - "nice, developer", - "Send Mail", - }, - { - "Jim Green ", - "42", - "London No. 1 Lake Park", - "wow", - "Send Mail", - }, - { - "Joe Black ", - "32", - "Sidney No. 1 Lake Park", - "cool, teacher", - "Send Mail", - }, - }, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nNameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/theme.go b/theme.go deleted file mode 100644 index 85016a5..0000000 --- a/theme.go +++ /dev/null @@ -1,332 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -const ThemeDark = "dark" -const ThemeLight = "light" -const ThemeGrafana = "grafana" -const ThemeAnt = "ant" - -type ColorPalette interface { - IsDark() bool - GetAxisStrokeColor() Color - SetAxisStrokeColor(Color) - GetAxisSplitLineColor() Color - SetAxisSplitLineColor(Color) - GetSeriesColor(int) Color - SetSeriesColor([]Color) - GetBackgroundColor() Color - SetBackgroundColor(Color) - GetTextColor() Color - SetTextColor(Color) - GetFontSize() float64 - SetFontSize(float64) - GetFont() *truetype.Font - SetFont(*truetype.Font) -} - -type themeColorPalette struct { - isDarkMode bool - axisStrokeColor Color - axisSplitLineColor Color - backgroundColor Color - textColor Color - seriesColors []Color - fontSize float64 - font *truetype.Font -} - -type ThemeOption struct { - IsDarkMode bool - AxisStrokeColor Color - AxisSplitLineColor Color - BackgroundColor Color - TextColor Color - SeriesColors []Color -} - -var palettes = map[string]*themeColorPalette{} - -const defaultFontSize = 12.0 - -var defaultTheme ColorPalette - -var defaultLightFontColor = drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, -} -var defaultDarkFontColor = drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, -} - -func init() { - echartSeriesColors := []Color{ - parseColor("#5470c6"), - parseColor("#91cc75"), - parseColor("#fac858"), - parseColor("#ee6666"), - parseColor("#73c0de"), - parseColor("#3ba272"), - parseColor("#fc8452"), - parseColor("#9a60b4"), - parseColor("#ea7ccc"), - } - grafanaSeriesColors := []Color{ - parseColor("#7EB26D"), - parseColor("#EAB839"), - parseColor("#6ED0E0"), - parseColor("#EF843C"), - parseColor("#E24D42"), - parseColor("#1F78C1"), - parseColor("#705DA0"), - parseColor("#508642"), - } - antSeriesColors := []Color{ - parseColor("#5b8ff9"), - parseColor("#5ad8a6"), - parseColor("#5d7092"), - parseColor("#f6bd16"), - parseColor("#6f5ef9"), - parseColor("#6dc8ec"), - parseColor("#945fb9"), - parseColor("#ff9845"), - } - AddTheme( - ThemeDark, - ThemeOption{ - IsDarkMode: true, - AxisStrokeColor: Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - AxisSplitLineColor: Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, - BackgroundColor: Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, - TextColor: Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, - SeriesColors: echartSeriesColors, - }, - ) - - AddTheme( - ThemeLight, - ThemeOption{ - IsDarkMode: false, - AxisStrokeColor: Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - AxisSplitLineColor: Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - BackgroundColor: drawing.ColorWhite, - TextColor: Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - SeriesColors: echartSeriesColors, - }, - ) - AddTheme( - ThemeAnt, - ThemeOption{ - IsDarkMode: false, - AxisStrokeColor: Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - AxisSplitLineColor: Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - BackgroundColor: drawing.ColorWhite, - TextColor: drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - SeriesColors: antSeriesColors, - }, - ) - AddTheme( - ThemeGrafana, - ThemeOption{ - IsDarkMode: true, - AxisStrokeColor: Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - AxisSplitLineColor: Color{ - R: 68, - G: 67, - B: 67, - A: 255, - }, - BackgroundColor: drawing.Color{ - R: 31, - G: 29, - B: 29, - A: 255, - }, - TextColor: Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - SeriesColors: grafanaSeriesColors, - }, - ) - SetDefaultTheme(ThemeLight) -} - -// SetDefaultTheme sets default theme -func SetDefaultTheme(name string) { - defaultTheme = NewTheme(name) -} - -func AddTheme(name string, opt ThemeOption) { - palettes[name] = &themeColorPalette{ - isDarkMode: opt.IsDarkMode, - axisStrokeColor: opt.AxisStrokeColor, - axisSplitLineColor: opt.AxisSplitLineColor, - backgroundColor: opt.BackgroundColor, - textColor: opt.TextColor, - seriesColors: opt.SeriesColors, - } -} - -func NewTheme(name string) ColorPalette { - p, ok := palettes[name] - if !ok { - p = palettes[ThemeLight] - } - clone := *p - return &clone -} - -func (t *themeColorPalette) IsDark() bool { - return t.isDarkMode -} - -func (t *themeColorPalette) GetAxisStrokeColor() Color { - return t.axisStrokeColor -} - -func (t *themeColorPalette) SetAxisStrokeColor(c Color) { - t.axisStrokeColor = c -} - -func (t *themeColorPalette) GetAxisSplitLineColor() Color { - return t.axisSplitLineColor -} - -func (t *themeColorPalette) SetAxisSplitLineColor(c Color) { - t.axisSplitLineColor = c -} - -func (t *themeColorPalette) GetSeriesColor(index int) Color { - colors := t.seriesColors - return colors[index%len(colors)] -} -func (t *themeColorPalette) SetSeriesColor(colors []Color) { - t.seriesColors = colors -} - -func (t *themeColorPalette) GetBackgroundColor() Color { - return t.backgroundColor -} - -func (t *themeColorPalette) SetBackgroundColor(c Color) { - t.backgroundColor = c -} - -func (t *themeColorPalette) GetTextColor() Color { - return t.textColor -} - -func (t *themeColorPalette) SetTextColor(c Color) { - t.textColor = c -} - -func (t *themeColorPalette) GetFontSize() float64 { - if t.fontSize != 0 { - return t.fontSize - } - return defaultFontSize -} - -func (t *themeColorPalette) SetFontSize(fontSize float64) { - t.fontSize = fontSize -} - -func (t *themeColorPalette) GetFont() *truetype.Font { - if t.font != nil { - return t.font - } - f, _ := GetDefaultFont() - return f -} - -func (t *themeColorPalette) SetFont(f *truetype.Font) { - t.font = f -} diff --git a/title.go b/title.go deleted file mode 100644 index 74ab4f9..0000000 --- a/title.go +++ /dev/null @@ -1,197 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strconv" - "strings" - - "github.com/golang/freetype/truetype" -) - -type TitleOption struct { - // The theme of chart - Theme ColorPalette - // Title text, support \n for new line - Text string - // Subtitle text, support \n for new line - Subtext string - // Distance between title component and the left side of the container. - // It can be pixel value: 20, percentage value: 20%, - // or position value: right, center. - Left string - // Distance between title component and the top side of the container. - // It can be pixel value: 20. - Top string - // The font of label - Font *truetype.Font - // The font size of label - FontSize float64 - // The color of label - FontColor Color - // The subtext font size of label - SubtextFontSize float64 - // The subtext font color of label - SubtextFontColor Color -} - -type titleMeasureOption struct { - width int - height int - text string - style Style -} - -func splitTitleText(text string) []string { - arr := strings.Split(text, "\n") - result := make([]string, 0) - for _, v := range arr { - v = strings.TrimSpace(v) - if v == "" { - continue - } - result = append(result, v) - } - return result -} - -type titlePainter struct { - p *Painter - opt *TitleOption -} - -// NewTitlePainter returns a title renderer -func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter { - return &titlePainter{ - p: p, - opt: &opt, - } -} - -func (t *titlePainter) Render() (Box, error) { - opt := t.opt - p := t.p - theme := opt.Theme - - if theme == nil { - theme = p.theme - } - if opt.Text == "" && opt.Subtext == "" { - return BoxZero, nil - } - - measureOptions := make([]titleMeasureOption, 0) - - if opt.Font == nil { - opt.Font = theme.GetFont() - } - if opt.FontColor.IsZero() { - opt.FontColor = theme.GetTextColor() - } - if opt.FontSize == 0 { - opt.FontSize = theme.GetFontSize() - } - if opt.SubtextFontColor.IsZero() { - opt.SubtextFontColor = opt.FontColor - } - if opt.SubtextFontSize == 0 { - opt.SubtextFontSize = opt.FontSize - } - - titleTextStyle := Style{ - Font: opt.Font, - FontSize: opt.FontSize, - FontColor: opt.FontColor, - } - // 主标题 - for _, v := range splitTitleText(opt.Text) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: titleTextStyle, - }) - } - subtextStyle := Style{ - Font: opt.Font, - FontSize: opt.SubtextFontSize, - FontColor: opt.SubtextFontColor, - } - // 副标题 - for _, v := range splitTitleText(opt.Subtext) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: subtextStyle, - }) - } - textMaxWidth := 0 - textMaxHeight := 0 - for index, item := range measureOptions { - p.OverrideTextStyle(item.style) - textBox := p.MeasureText(item.text) - - w := textBox.Width() - h := textBox.Height() - if w > textMaxWidth { - textMaxWidth = w - } - if h > textMaxHeight { - textMaxHeight = h - } - measureOptions[index].height = h - measureOptions[index].width = w - } - width := textMaxWidth - - titleX := 0 - switch opt.Left { - case PositionRight: - titleX = p.Width() - textMaxWidth - case PositionCenter: - titleX = p.Width()>>1 - (textMaxWidth >> 1) - default: - if strings.HasSuffix(opt.Left, "%") { - value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) - titleX = p.Width() * value / 100 - } else { - value, _ := strconv.Atoi(opt.Left) - titleX = value - } - } - titleY := 0 - // TODO TOP 暂只支持数值 - if opt.Top != "" { - value, _ := strconv.Atoi(opt.Top) - titleY += value - } - for _, item := range measureOptions { - p.OverrideTextStyle(item.style) - x := titleX + (textMaxWidth-item.width)>>1 - y := titleY + item.height - p.Text(item.text, x, y) - titleY += item.height - } - - return Box{ - Bottom: titleY, - Right: titleX + width, - }, nil -} diff --git a/title_test.go b/title_test.go deleted file mode 100644 index add8163..0000000 --- a/title_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTitleRenderer(t *testing.T) { - assert := assert.New(t) - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: "20", - Top: "20", - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: "20%", - Top: "20", - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - { - render: func(p *Painter) ([]byte, error) { - _, err := NewTitlePainter(p, TitleOption{ - Text: "title", - Subtext: "subTitle", - Left: PositionRight, - }).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\ntitlesubTitle", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme)) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/util.go b/util.go deleted file mode 100644 index 87ff31c..0000000 --- a/util.go +++ /dev/null @@ -1,271 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - "regexp" - "strconv" - "strings" - - "github.com/dustin/go-humanize" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TrueFlag() *bool { - t := true - return &t -} - -func FalseFlag() *bool { - f := false - return &f -} - -func containsInt(values []int, value int) bool { - for _, v := range values { - if v == value { - return true - } - } - return false -} - -func containsString(values []string, value string) bool { - for _, v := range values { - if v == value { - return true - } - } - return false -} - -func ceilFloatToInt(value float64) int { - i := int(value) - if value == float64(i) { - return i - } - return i + 1 -} - -func getDefaultInt(value, defaultValue int) int { - if value == 0 { - return defaultValue - } - return value -} - -func autoDivide(max, size int) []int { - unit := float64(max) / float64(size) - - values := make([]int, size+1) - for i := 0; i < size+1; i++ { - if i == size { - values[i] = max - } else { - values[i] = int(float64(i) * unit) - } - } - return values -} - -func autoDivideSpans(max, size int, spans []int) []int { - values := autoDivide(max, size) - // 重新合并 - if len(spans) != 0 { - newValues := make([]int, len(spans)+1) - newValues[0] = 0 - end := 0 - for index, v := range spans { - end += v - newValues[index+1] = values[end] - } - values = newValues - } - return values -} - -func sumInt(values []int) int { - sum := 0 - for _, v := range values { - sum += v - } - return sum -} - -// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list -func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { - maxWidth := 0 - maxHeight := 0 - for _, text := range textList { - box := p.MeasureText(text) - maxWidth = chart.MaxInt(maxWidth, box.Width()) - maxHeight = chart.MaxInt(maxHeight, box.Height()) - } - return maxWidth, maxHeight -} - -func reverseStringSlice(stringList []string) { - for i, j := 0, len(stringList)-1; i < j; i, j = i+1, j-1 { - stringList[i], stringList[j] = stringList[j], stringList[i] - } -} - -func reverseIntSlice(intList []int) { - for i, j := 0, len(intList)-1; i < j; i, j = i+1, j-1 { - intList[i], intList[j] = intList[j], intList[i] - } -} - -func convertPercent(value string) float64 { - if !strings.HasSuffix(value, "%") { - return -1 - } - v, err := strconv.Atoi(strings.ReplaceAll(value, "%", "")) - if err != nil { - return -1 - } - return float64(v) / 100 -} - -func isFalse(flag *bool) bool { - if flag != nil && !*flag { - return true - } - return false -} - -func NewFloatPoint(f float64) *float64 { - v := f - return &v -} - -const K_VALUE = float64(1000) -const M_VALUE = K_VALUE * K_VALUE -const G_VALUE = M_VALUE * K_VALUE -const T_VALUE = G_VALUE * K_VALUE - -func commafWithDigits(value float64) string { - decimals := 2 - if value >= T_VALUE { - return humanize.CommafWithDigits(value/T_VALUE, decimals) + "T" - } - if value >= G_VALUE { - return humanize.CommafWithDigits(value/G_VALUE, decimals) + "G" - } - if value >= M_VALUE { - return humanize.CommafWithDigits(value/M_VALUE, decimals) + "M" - } - if value >= K_VALUE { - return humanize.CommafWithDigits(value/K_VALUE, decimals) + "k" - } - return humanize.CommafWithDigits(value, decimals) -} - -func parseColor(color string) Color { - c := Color{} - if color == "" { - return c - } - if strings.HasPrefix(color, "#") { - return drawing.ColorFromHex(color[1:]) - } - reg := regexp.MustCompile(`\((\S+)\)`) - result := reg.FindAllStringSubmatch(color, 1) - if len(result) == 0 || len(result[0]) != 2 { - return c - } - arr := strings.Split(result[0][1], ",") - if len(arr) < 3 { - return c - } - // 设置默认为255 - c.A = 255 - for index, v := range arr { - value, _ := strconv.Atoi(strings.TrimSpace(v)) - ui8 := uint8(value) - switch index { - case 0: - c.R = ui8 - case 1: - c.G = ui8 - case 2: - c.B = ui8 - default: - c.A = ui8 - } - } - return c -} - -const defaultRadiusPercent = 0.4 - -func getRadius(diameter float64, radiusValue string) float64 { - var radius float64 - if len(radiusValue) != 0 { - v := convertPercent(radiusValue) - if v != -1 { - radius = float64(diameter) * v - } else { - radius, _ = strconv.ParseFloat(radiusValue, 64) - } - } - if radius <= 0 { - radius = float64(diameter) * defaultRadiusPercent - } - return radius -} - -func getPolygonPointAngles(sides int) []float64 { - angles := make([]float64, sides) - for i := 0; i < sides; i++ { - angle := 2*math.Pi/float64(sides)*float64(i) - (math.Pi / 2) - angles[i] = angle - } - return angles -} - -func getPolygonPoint(center Point, radius, angle float64) Point { - x := center.X + int(radius*math.Cos(angle)) - y := center.Y + int(radius*math.Sin(angle)) - return Point{ - X: x, - Y: y, - } -} - -func getPolygonPoints(center Point, radius float64, sides int) []Point { - points := make([]Point, sides) - for i, angle := range getPolygonPointAngles(sides) { - points[i] = getPolygonPoint(center, radius, angle) - } - return points -} - -func isLightColor(c Color) bool { - r := float64(c.R) * float64(c.R) * 0.299 - g := float64(c.G) * float64(c.G) * 0.587 - b := float64(c.B) * float64(c.B) * 0.114 - return math.Sqrt(r+g+b) > 127.5 -} diff --git a/util_test.go b/util_test.go deleted file mode 100644 index 5770776..0000000 --- a/util_test.go +++ /dev/null @@ -1,223 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "git.smarteching.com/zeni/go-chart/v2" - "git.smarteching.com/zeni/go-chart/v2/drawing" -) - -func TestGetDefaultInt(t *testing.T) { - assert := assert.New(t) - - assert.Equal(1, getDefaultInt(0, 1)) - assert.Equal(10, getDefaultInt(10, 1)) -} - -func TestCeilFloatToInt(t *testing.T) { - assert := assert.New(t) - - assert.Equal(1, ceilFloatToInt(0.8)) - assert.Equal(1, ceilFloatToInt(1.0)) - assert.Equal(2, ceilFloatToInt(1.2)) -} - -func TestCommafWithDigits(t *testing.T) { - assert := assert.New(t) - - assert.Equal("1.2", commafWithDigits(1.2)) - assert.Equal("1.21", commafWithDigits(1.21231)) - - assert.Equal("1.20k", commafWithDigits(1200.121)) - assert.Equal("1.20M", commafWithDigits(1200000.121)) -} - -func TestAutoDivide(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]int{ - 0, - 85, - 171, - 257, - 342, - 428, - 514, - 600, - }, autoDivide(600, 7)) -} - -func TestGetRadius(t *testing.T) { - assert := assert.New(t) - - assert.Equal(50.0, getRadius(100, "50%")) - assert.Equal(30.0, getRadius(100, "30")) - assert.Equal(40.0, getRadius(100, "")) -} - -func TestMeasureTextMaxWidthHeight(t *testing.T) { - assert := assert.New(t) - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - style := chart.Style{ - FontSize: 10, - } - p.SetStyle(style) - - maxWidth, maxHeight := measureTextMaxWidthHeight([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, p) - assert.Equal(31, maxWidth) - assert.Equal(12, maxHeight) -} - -func TestReverseSlice(t *testing.T) { - assert := assert.New(t) - - arr := []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - } - reverseStringSlice(arr) - assert.Equal([]string{ - "Sun", - "Sat", - "Fri", - "Thu", - "Wed", - "Tue", - "Mon", - }, arr) - - numbers := []int{ - 1, - 3, - 5, - 7, - 9, - } - reverseIntSlice(numbers) - assert.Equal([]int{ - 9, - 7, - 5, - 3, - 1, - }, numbers) -} - -func TestConvertPercent(t *testing.T) { - assert := assert.New(t) - - assert.Equal(-1.0, convertPercent("1")) - assert.Equal(-1.0, convertPercent("a%")) - assert.Equal(0.1, convertPercent("10%")) -} - -func TestParseColor(t *testing.T) { - assert := assert.New(t) - - c := parseColor("") - assert.True(c.IsZero()) - - c = parseColor("#333") - assert.Equal(drawing.Color{ - R: 51, - G: 51, - B: 51, - A: 255, - }, c) - - c = parseColor("#313233") - assert.Equal(drawing.Color{ - R: 49, - G: 50, - B: 51, - A: 255, - }, c) - - c = parseColor("rgb(31,32,33)") - assert.Equal(drawing.Color{ - R: 31, - G: 32, - B: 33, - A: 255, - }, c) - - c = parseColor("rgba(50,51,52,250)") - assert.Equal(drawing.Color{ - R: 50, - G: 51, - B: 52, - A: 250, - }, c) -} - -func TestIsLightColor(t *testing.T) { - assert := assert.New(t) - - assert.True(isLightColor(drawing.Color{ - R: 255, - G: 255, - B: 255, - })) - assert.True(isLightColor(drawing.Color{ - R: 145, - G: 204, - B: 117, - })) - - assert.False(isLightColor(drawing.Color{ - R: 88, - G: 112, - B: 198, - })) - - assert.False(isLightColor(drawing.Color{ - R: 0, - G: 0, - B: 0, - })) - assert.False(isLightColor(drawing.Color{ - R: 16, - G: 12, - B: 42, - })) -} diff --git a/web/index.css b/web/index.css new file mode 100644 index 0000000..7709f9b --- /dev/null +++ b/web/index.css @@ -0,0 +1,71 @@ +body { + font-family: BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif; + height: 100vh; + background-color: #242424; +} +.header { + height: 60px; + line-height: 60px; + color: #fff; + font-size: 16px; + background-color: #383838; + text-indent: 2em; +} +.header span { + margin-left: 50px; + margin-right: 5px; +} +.codeWrapper { + position: fixed; + left: 0; + right: 50%; + top: 60px; + bottom: 50px; + background-color: #d6dbe3; +} +.previewWrapper { + position: fixed; + left: 50%; + right: 0; + top: 60px; + bottom: 50px; +} +.optionTips { + position: absolute; + top: 3px; + right: 10px; +} +.previewTips { + position: absolute; + top: 3px; + left: 10px; + color: #fff; +} +.run { + display: block; + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: 50px; + line-height: 50px; + background-color: #0052D9; + color: #fff; + text-align: center; + font-size: 16px; + text-decoration: none; +} +.run:active, .run:visited { + color: #fff; +} +#svg { + position: absolute; + left: 0; + right: 0; + top: 50%; + margin-top: -200px; +} +#svg svg, #svg img { + display: block; + margin: auto; +} \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4de3b5c --- /dev/null +++ b/web/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + +
Go Charts + 选择图表输出格式: + +
+
+ +
ECharts配置
+
+
+
+
图表SVG效果
+
+ 运行 + + + + \ No newline at end of file diff --git a/web/index.js b/web/index.js new file mode 100644 index 0000000..7cfe3bb --- /dev/null +++ b/web/index.js @@ -0,0 +1,50 @@ +var height = document.body.clientHeight- 110; +var editor = CodeMirror.fromTextArea(document.getElementById("codeInput"), { + lineNumbers: true, + lineWrapping: true, + mode: "javascript" +}); +editor.setSize("100%", height); +editor.setValue(`option = { + xAxis: { + type: 'category', + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + yAxis: { + type: 'value' + }, + series: [ + { + data: [150, 230, 224, 218, 135, 147, 260], + type: 'line' + } + ] +};`); + +function run() { + var option = editor.getValue(); + var data = null; + try { + if (option.indexOf("option = ") !== -1) { + var fn = new Function("var " + option + ";return option;"); + data = fn(); + } else { + data = JSON.parse(option); + } + } catch (err) { + alert(err.message); + return; + } + var dom = document.getElementById("outputType") + var outputType = dom.value; + + axios.post("/?outputType=" + outputType, data).then(function(resp) { + if (outputType == "png") { + document.getElementById("svg").innerHTML = ''; + } else { + document.getElementById("svg").innerHTML = resp; + } + }).catch(function(err) { + alert(err.message); + }); +} \ No newline at end of file diff --git a/web/javascript.js b/web/javascript.js new file mode 100644 index 0000000..09ba4c3 --- /dev/null +++ b/web/javascript.js @@ -0,0 +1,959 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); + })(function(CodeMirror) { + "use strict"; + + CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var trackScope = parserConfig.trackScope !== false + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + return { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, + "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, + "await": C + }; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^@]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (expressionAllowed(stream, state, 1)) { + readRegexp(stream); + stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eat("="); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#" && stream.peek() == "!") { + stream.skipToEnd(); + return ret("meta", "meta"); + } else if (ch == "#" && stream.eatWhile(wordRE)) { + return ret("variable", "property") + } else if (ch == "<" && stream.match("!--") || + (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { + stream.skipToEnd() + return ret("comment", "comment") + } else if (isOperatorChar.test(ch)) { + if (ch != ">" || !state.lexical || state.lexical.type != ">") { + if (stream.eat("=")) { + if (ch == "!" || ch == "=") stream.eat("=") + } else if (/[<>*+\-|&?]/.test(ch)) { + stream.eat(ch) + if (ch == ">") stream.eat(ch) + } + } + if (ch == "?" && stream.eat(".")) return ret(".") + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current() + if (state.lastType != ".") { + if (keywords.propertyIsEnumerable(word)) { + var kw = keywords[word] + return ret(kw.type, kw.style, word) + } + if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false)) + return ret("async", "keyword", word) + } + return ret("variable", "variable", word) + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + if (isTS) { // Try to skip TypeScript return type declarations after the arguments + var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) + if (m) arrow = m.index + } + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) { if (ch == "(") sawSomething = true; break; } + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/`]/.test(ch)) { + for (;; --pos) { + if (pos == 0) return + var next = stream.string.charAt(pos - 1) + if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } + } + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, + "regexp": true, "this": true, "import": true, "jsonld-keyword": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + if (!trackScope) return false + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function inList(name, list) { + for (var v = list; v; v = v.next) if (v.name == name) return true + return false; + } + function register(varname) { + var state = cx.state; + cx.marked = "def"; + if (!trackScope) return + if (state.context) { + if (state.lexical.info == "var" && state.context && state.context.block) { + // FIXME function decls are also not block scoped + var newContext = registerVarScoped(varname, state.context) + if (newContext != null) { + state.context = newContext + return + } + } else if (!inList(varname, state.localVars)) { + state.localVars = new Var(varname, state.localVars) + return + } + } + // Fall through means this is global + if (parserConfig.globalVars && !inList(varname, state.globalVars)) + state.globalVars = new Var(varname, state.globalVars) + } + function registerVarScoped(varname, context) { + if (!context) { + return null + } else if (context.block) { + var inner = registerVarScoped(varname, context.prev) + if (!inner) return null + if (inner == context.prev) return context + return new Context(inner, context.vars, true) + } else if (inList(varname, context.vars)) { + return context + } else { + return new Context(context.prev, new Var(varname, context.vars), false) + } + } + + function isModifier(name) { + return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" + } + + // Combinators + + function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } + function Var(name, next) { this.name = name; this.next = next } + + var defaultVars = new Var("this", new Var("arguments", null)) + function pushcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, false) + cx.state.localVars = defaultVars + } + function pushblockcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, true) + cx.state.localVars = null + } + function popcontext() { + cx.state.localVars = cx.state.context.vars + cx.state.context = cx.state.context.prev + } + popcontext.lex = true + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + function exp(type) { + if (type == wanted) return cont(); + else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); + else return cont(exp); + }; + return exp; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); + if (type == "debugger") return cont(expect(";")); + if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); + if (type == ";") return cont(); + if (type == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); + } + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex); + if (type == "class" || (isTS && value == "interface")) { + cx.marked = "keyword" + return cont(pushlex("form", type == "class" ? type : value), className, poplex) + } + if (type == "variable") { + if (isTS && value == "declare") { + cx.marked = "keyword" + return cont(statement) + } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { + cx.marked = "keyword" + if (value == "enum") return cont(enumdef); + else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); + else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) + } else if (isTS && value == "namespace") { + cx.marked = "keyword" + return cont(pushlex("form"), expression, statement, poplex) + } else if (isTS && value == "abstract") { + cx.marked = "keyword" + return cont(statement) + } else { + return cont(pushlex("stat"), maybelabel); + } + } + if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, + block, poplex, poplex, popcontext); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); + if (type == "export") return cont(pushlex("stat"), afterExport, poplex); + if (type == "import") return cont(pushlex("stat"), afterImport, poplex); + if (type == "async") return cont(statement) + if (value == "@") return cont(expression, statement) + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function maybeCatchBinding(type) { + if (type == "(") return cont(funarg, expect(")")) + } + function expression(type, value) { + return expressionInner(type, value, false); + } + function expressionNoComma(type, value) { + return expressionInner(type, value, true); + } + function parenExpr(type) { + if (type != "(") return pass() + return cont(pushlex(")"), maybeexpression, expect(")"), poplex) + } + function expressionInner(type, value, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef, maybeop); + if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } + if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); + if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + if (type == "quasi") return pass(quasi, maybeop); + if (type == "new") return cont(maybeTarget(noComma)); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(maybeexpression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); + if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { return pass(quasi, me); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } + if (type == "regexp") { + cx.state.lastType = cx.marked = "operator" + cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) + return cont(expr) + } + } + function quasi(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasi); + return cont(maybeexpression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expressionNoComma); + } + function maybeTarget(noComma) { + return function(type) { + if (type == ".") return cont(noComma ? targetNoComma : target); + else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) + else return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } + } + function targetNoComma(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "async") { + cx.marked = "property"; + return cont(objprop); + } else if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params + if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) + cx.state.fatArrowAt = cx.stream.pos + m[0].length + return cont(afterprop); + } else if (type == "number" || type == "string") { + cx.marked = jsonldMode ? "property" : (cx.style + " property"); + return cont(afterprop); + } else if (type == "jsonld-keyword") { + return cont(afterprop); + } else if (isTS && isModifier(value)) { + cx.marked = "keyword" + return cont(objprop) + } else if (type == "[") { + return cont(expression, maybetype, expect("]"), afterprop); + } else if (type == "spread") { + return cont(expressionNoComma, afterprop); + } else if (value == "*") { + cx.marked = "keyword"; + return cont(objprop); + } else if (type == ":") { + return pass(afterprop) + } + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end, sep) { + function proceed(type, value) { + if (sep ? sep.indexOf(type) > -1 : type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(function(type, value) { + if (type == end || value == end) return pass() + return pass(what) + }, proceed); + } + if (type == end || value == end) return cont(); + if (sep && sep.indexOf(";") > -1) return pass(what) + return cont(expect(end)); + } + return function(type, value) { + if (type == end || value == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type, value) { + if (isTS) { + if (type == ":") return cont(typeexpr); + if (value == "?") return cont(maybetype); + } + } + function maybetypeOrIn(type, value) { + if (isTS && (type == ":" || value == "in")) return cont(typeexpr) + } + function mayberettype(type) { + if (isTS && type == ":") { + if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) + else return cont(typeexpr) + } + } + function isKW(_, value) { + if (value == "is") { + cx.marked = "keyword" + return cont() + } + } + function typeexpr(type, value) { + if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") { + cx.marked = "keyword" + return cont(value == "typeof" ? expressionNoComma : typeexpr) + } + if (type == "variable" || value == "void") { + cx.marked = "type" + return cont(afterType) + } + if (value == "|" || value == "&") return cont(typeexpr) + if (type == "string" || type == "number" || type == "atom") return cont(afterType); + if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) + if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType) + if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) + if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) + if (type == "quasi") { return pass(quasiType, afterType); } + } + function maybeReturnType(type) { + if (type == "=>") return cont(typeexpr) + } + function typeprops(type) { + if (type.match(/[\}\)\]]/)) return cont() + if (type == "," || type == ";") return cont(typeprops) + return pass(typeprop, typeprops) + } + function typeprop(type, value) { + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property" + return cont(typeprop) + } else if (value == "?" || type == "number" || type == "string") { + return cont(typeprop) + } else if (type == ":") { + return cont(typeexpr) + } else if (type == "[") { + return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) + } else if (type == "(") { + return pass(functiondecl, typeprop) + } else if (!type.match(/[;\}\)\],]/)) { + return cont() + } + } + function quasiType(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasiType); + return cont(typeexpr, continueQuasiType); + } + function continueQuasiType(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasiType); + } + } + function typearg(type, value) { + if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) + if (type == ":") return cont(typeexpr) + if (type == "spread") return cont(typearg) + return pass(typeexpr) + } + function afterType(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + if (value == "|" || type == "." || value == "&") return cont(typeexpr) + if (type == "[") return cont(typeexpr, expect("]"), afterType) + if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } + if (value == "?") return cont(typeexpr, expect(":"), typeexpr) + } + function maybeTypeArgs(_, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + } + function typeparam() { + return pass(typeexpr, maybeTypeDefault) + } + function maybeTypeDefault(_, value) { + if (value == "=") return cont(typeexpr) + } + function vardef(_, value) { + if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } + if (type == "variable") { register(value); return cont(); } + if (type == "spread") return cont(pattern); + if (type == "[") return contCommasep(eltpattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + if (type == "spread") return cont(pattern); + if (type == "}") return pass(); + if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); + return cont(expect(":"), pattern, maybeAssign); + } + function eltpattern() { + return pass(pattern, maybeAssign) + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type, value) { + if (value == "await") return cont(forspec); + if (type == "(") return cont(pushlex(")"), forspec1, poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, forspec2); + if (type == "variable") return cont(forspec2); + return pass(forspec2) + } + function forspec2(type, value) { + if (type == ")") return cont() + if (type == ";") return cont(forspec2) + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } + return pass(expression, forspec2) + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) + } + function functiondecl(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} + if (type == "variable") {register(value); return cont(functiondecl);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) + } + function typename(type, value) { + if (type == "keyword" || type == "variable") { + cx.marked = "type" + return cont(typename) + } else if (value == "<") { + return cont(pushlex(">"), commasep(typeparam, ">"), poplex) + } + } + function funarg(type, value) { + if (value == "@") cont(expression, funarg) + if (type == "spread") return cont(funarg); + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } + if (isTS && type == "this") return cont(maybetype, maybeAssign) + return pass(pattern, maybetype, maybeAssign); + } + function classExpression(type, value) { + // Class expressions may have an optional name. + if (type == "variable") return className(type, value); + return classNameAfter(type, value); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) + if (value == "extends" || value == "implements" || (isTS && type == ",")) { + if (value == "implements") cx.marked = "keyword"; + return cont(isTS ? typeexpr : expression, classNameAfter); + } + if (type == "{") return cont(pushlex("}"), classBody, poplex); + } + function classBody(type, value) { + if (type == "async" || + (type == "variable" && + (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && + cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(classfield, classBody); + } + if (type == "number" || type == "string") return cont(classfield, classBody); + if (type == "[") + return cont(expression, maybetype, expect("]"), classfield, classBody) + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (isTS && type == "(") return pass(functiondecl, classBody) + if (type == ";" || type == ",") return cont(classBody); + if (type == "}") return cont(); + if (value == "@") return cont(expression, classBody) + } + function classfield(type, value) { + if (value == "!") return cont(classfield) + if (value == "?") return cont(classfield) + if (type == ":") return cont(typeexpr, maybeAssign) + if (value == "=") return cont(expressionNoComma) + var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" + return pass(isInterface ? functiondecl : functiondef) + } + function afterExport(type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); + return pass(statement); + } + function exportField(type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } + if (type == "variable") return pass(expressionNoComma, exportField); + } + function afterImport(type) { + if (type == "string") return cont(); + if (type == "(") return pass(expression); + if (type == ".") return pass(maybeoperatorComma); + return pass(importSpec, maybeMoreImports, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + if (value == "*") cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeMoreImports(type) { + if (type == ",") return cont(importSpec, maybeMoreImports) + } + function maybeAs(_type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(commasep(expressionNoComma, "]")); + } + function enumdef() { + return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) + } + function enummember() { + return pass(pattern, maybeAssign); + } + + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || + isOperatorChar.test(textAfter.charAt(0)) || + /[,.]/.test(textAfter.charAt(0)); + } + + function expressionAllowed(stream, state, backUp) { + return state.tokenize == tokenBase && + /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || + (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && new Context(null, null, false), + indented: basecolumn || 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top + // Kludge to prevent 'maybelse' from blocking lexical scope pops + if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse && c != popcontext) break; + } + while ((lexical.type == "stat" || lexical.type == "form") && + (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && + (top == maybeoperatorComma || top == maybeoperatorNoComma) && + !/^[,\.=+\-*:?[\(]/.test(textAfter)))) + lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + blockCommentContinue: jsonMode ? null : " * ", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + + helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, + jsonMode: jsonMode, + + expressionAllowed: expressionAllowed, + + skipExpression: function(state) { + parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null)) + } + }; + }); + +// CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + + CodeMirror.defineMIME("text/javascript", "javascript"); + CodeMirror.defineMIME("text/ecmascript", "javascript"); + CodeMirror.defineMIME("application/javascript", "javascript"); + CodeMirror.defineMIME("application/x-javascript", "javascript"); + CodeMirror.defineMIME("application/ecmascript", "javascript"); + CodeMirror.defineMIME("application/json", { name: "javascript", json: true }); + CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true }); + CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true }) + CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true }); + CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); + CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + + }); \ No newline at end of file diff --git a/xaxis.go b/xaxis.go deleted file mode 100644 index 61698d7..0000000 --- a/xaxis.go +++ /dev/null @@ -1,105 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" -) - -type XAxisOption struct { - // The font of x axis - Font *truetype.Font - // The boundary gap on both sides of a coordinate axis. - // Nil or *true means the center part of two axis ticks - BoundaryGap *bool - // The data value of x axis - Data []string - // The theme of chart - Theme ColorPalette - // The font size of x axis label - FontSize float64 - // The flag for show axis, set this to *false will hide axis - Show *bool - // Number of segments that the axis is split into. Note that this number serves only as a recommendation. - SplitNumber int - // The position of axis, it can be 'top' or 'bottom' - Position string - // The line color of axis - StrokeColor Color - // The color of label - FontColor Color - // The text rotation of label - TextRotation float64 - // The first axis - FirstAxis int - // The offset of label - LabelOffset Box - isValueAxis bool -} - -const defaultXAxisHeight = 30 - -// NewXAxisOption returns a x axis option -func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { - opt := XAxisOption{ - Data: data, - } - if len(boundaryGap) != 0 { - opt.BoundaryGap = boundaryGap[0] - } - return opt -} - -func (opt *XAxisOption) ToAxisOption() AxisOption { - position := PositionBottom - if opt.Position == PositionTop { - position = PositionTop - } - axisOpt := AxisOption{ - Theme: opt.Theme, - Data: opt.Data, - BoundaryGap: opt.BoundaryGap, - Position: position, - SplitNumber: opt.SplitNumber, - StrokeColor: opt.StrokeColor, - FontSize: opt.FontSize, - Font: opt.Font, - FontColor: opt.FontColor, - Show: opt.Show, - SplitLineColor: opt.Theme.GetAxisSplitLineColor(), - TextRotation: opt.TextRotation, - LabelOffset: opt.LabelOffset, - FirstAxis: opt.FirstAxis, - } - if opt.isValueAxis { - axisOpt.SplitLineShow = true - axisOpt.StrokeWidth = -1 - axisOpt.BoundaryGap = FalseFlag() - } - return axisOpt -} - -// NewBottomXAxis returns a bottom x axis renderer -func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { - return NewAxisPainter(p, opt.ToAxisOption()) -} diff --git a/yaxis.go b/yaxis.go deleted file mode 100644 index e58b7a6..0000000 --- a/yaxis.go +++ /dev/null @@ -1,128 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import "github.com/golang/freetype/truetype" - -type YAxisOption struct { - // The minimun value of axis. - Min *float64 - // The maximum value of axis. - Max *float64 - // The font of y axis - Font *truetype.Font - // The data value of x axis - Data []string - // The theme of chart - Theme ColorPalette - // The font size of x axis label - FontSize float64 - // The position of axis, it can be 'left' or 'right' - Position string - // The color of label - FontColor Color - // Formatter for y axis text value - Formatter string - // Color for y axis - Color Color - // The flag for show axis, set this to *false will hide axis - Show *bool - DivideCount int - Unit int - isCategoryAxis bool - // The flag for show axis split line, set this to true will show axis split line - SplitLineShow *bool -} - -// NewYAxisOptions returns a y axis option -func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { - arr := [][]string{ - data, - } - arr = append(arr, others...) - opts := make([]YAxisOption, 0) - for _, data := range arr { - opts = append(opts, YAxisOption{ - Data: data, - }) - } - return opts -} - -func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { - position := PositionLeft - if opt.Position == PositionRight { - position = PositionRight - } - theme := opt.Theme - if theme == nil { - theme = p.theme - } - axisOpt := AxisOption{ - Formatter: opt.Formatter, - Theme: theme, - Data: opt.Data, - Position: position, - FontSize: opt.FontSize, - StrokeWidth: -1, - Font: opt.Font, - FontColor: opt.FontColor, - BoundaryGap: FalseFlag(), - SplitLineShow: true, - SplitLineColor: theme.GetAxisSplitLineColor(), - Show: opt.Show, - Unit: opt.Unit, - } - if !opt.Color.IsZero() { - axisOpt.FontColor = opt.Color - axisOpt.StrokeColor = opt.Color - } - if opt.isCategoryAxis { - axisOpt.BoundaryGap = TrueFlag() - axisOpt.StrokeWidth = 1 - axisOpt.SplitLineShow = false - } - if opt.SplitLineShow != nil { - axisOpt.SplitLineShow = *opt.SplitLineShow - } - return axisOpt -} - -// NewLeftYAxis returns a left y axis renderer -func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { - p = p.Child(PainterPaddingOption(Box{ - Bottom: defaultXAxisHeight, - })) - return NewAxisPainter(p, opt.ToAxisOption(p)) -} - -// NewRightYAxis returns a right y axis renderer -func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { - p = p.Child(PainterPaddingOption(Box{ - Bottom: defaultXAxisHeight, - })) - axisOpt := opt.ToAxisOption(p) - axisOpt.Position = PositionRight - axisOpt.SplitLineShow = false - return NewAxisPainter(p, axisOpt) -} diff --git a/yaxis_test.go b/yaxis_test.go deleted file mode 100644 index 0f565ac..0000000 --- a/yaxis_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRightYAxis(t *testing.T) { - assert := assert.New(t) - tests := []struct { - render func(*Painter) ([]byte, error) - result string - }{ - { - render: func(p *Painter) ([]byte, error) { - opt := NewYAxisOptions([]string{ - "a", - "b", - "c", - "d", - })[0] - _, err := NewRightYAxis(p, opt).Render() - if err != nil { - return nil, err - } - return p.Bytes() - }, - result: "\\nabcd", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Type: ChartOutputSVG, - Width: 600, - Height: 400, - }, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - })) - assert.Nil(err) - data, err := tt.render(p) - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -}