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
-
-[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
-[](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.
-
-
-
-
-
-
-
-
-
-## 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
-
-[](https://github.com/vicanso/go-charts/blob/master/LICENSE)
-[](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):
-
-
-
-
-
-
-
-
-
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: "",
- },
- // 底部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: "",
- },
- // 左侧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: "",
- },
- // 左侧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: "",
- },
- // 右侧
- {
- 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: "",
- },
- // 顶部
- {
- 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: "",
- },
- }
-
- 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: "",
- },
- {
- 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: "",
- },
- }
-
- 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("", 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("", 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("", 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("", 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("", 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("", 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("", 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: "",
- },
- }
-
- 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: "",
- },
- {
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- {
- 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: "",
- },
- {
- 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: "",
- },
- }
- 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: "",
- },
- {
- 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: "",
- },
- }
-
- 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: "",
- },
- }
- 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: "",
- },
- }
-
- 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: "",
- },
- // circle
- {
- fn: func(p *Painter) {
- p.Circle(5, 2, 3)
- },
- result: "",
- },
- // text
- {
- fn: func(p *Painter) {
- p.Text("hello world!", 3, 6)
- },
- result: "",
- },
- // 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: "",
- },
- // set background
- {
- fn: func(p *Painter) {
- p.SetBackground(400, 300, chart.ColorWhite)
- },
- result: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- // 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: "",
- },
- }
- 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("", 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(``, 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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: "",
- },
- {
- 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: "",
- },
- }
- 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: "",
- },
- {
- 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: "",
- },
- {
- 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: "",
- },
- }
- 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: "",
- },
- }
- 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))
- }
-}