Skip to content

Commit 72806a1

Browse files
committed
Add another line chart example, this one showing more use of the Painter
This example disables most of the default rendering (Y-Axis, legend), and instead renders the labels manually on the Painter.
1 parent 3a88761 commit 72806a1

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Examples are our primary method for demonstrating the starting point our use and
1414
* [line_chart-1](./line_chart-1) - Basic line chart with some simple styling changes and a demonstration of `null` values.
1515
* [line_chart-2](./line_chart-2) - The above example line chart re-demonstrated using the Painter API.
1616
* [line_chart-3](./line_chart-3) - Line chart with dense data and more custom styling configured.
17+
* [line_chart-4](./line_chart-4) - Line chart with dense data and most default rendering disabled, instead rendering labels manually on the Painter.
1718
* [line_chart-area](./line_chart-area) - Example line chart with the area below the line shaded.
1819
* [multiple_charts-1](./multiple_charts-1) - Example of manually building a painter so that you can render 4 charts on the same image.
1920
* [multiple_charts-2](./multiple_charts-2) - Combining two charts together by writting one chart over the other.

examples/line_chart-4/main.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sort"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/go-analyze/charts"
12+
"github.com/go-analyze/charts/chartdraw/drawing"
13+
)
14+
15+
/*
16+
Another line chart example with large data point counts, and use of gaps. This helps demonstrate additional
17+
configuration and styling, and notably includes custom drawing directly on the Painter.
18+
19+
The data shown here is an example of comparing the f-stop across multiple Canon lens options.
20+
*/
21+
22+
const startMM = 60
23+
const endMM = 510
24+
25+
var lensDefinitions = map[string]string{
26+
"70-200mm f/2.8": "70f2.8,201f-", // use a string encoding to define the f/stop point changes
27+
"70-200mm f/2.8 + 1.4x TC": "98f4,281f-",
28+
"70-200mm f/2.8 + 2x TC": "140f5.6,401f-",
29+
"100-500mm f/4.5-7.1": "100f4.5,151f5,254f5.6,363f6.3,472f7.1,501f-",
30+
//"200-800mm f/6.3-9": "200f6.3,268f7.1,455f8,637f9,801f-",
31+
}
32+
33+
func writeFile(buf []byte) error {
34+
tmpPath := "./tmp"
35+
if err := os.MkdirAll(tmpPath, 0700); err != nil {
36+
return err
37+
}
38+
39+
file := filepath.Join(tmpPath, "line-chart-4.png")
40+
return os.WriteFile(file, buf, 0600)
41+
}
42+
43+
func main() {
44+
values, xAxisLabels, labels, err := populateData()
45+
if err != nil {
46+
panic(err)
47+
}
48+
49+
opt := charts.LineChartOption{}
50+
opt.Theme = charts.GetTheme(charts.ThemeAnt)
51+
opt.Padding = charts.Box{
52+
Top: 20,
53+
Left: 20,
54+
Right: 20,
55+
Bottom: 10,
56+
}
57+
opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeLine)
58+
opt.Title.Text = "Canon RF Zoom Lenses"
59+
opt.Title.Offset = charts.OffsetCenter
60+
opt.Title.FontStyle.FontSize = 16
61+
62+
opt.XAxis.Data = xAxisLabels
63+
opt.XAxis.Unit = 40
64+
opt.XAxis.LabelCount = 10
65+
opt.XAxis.BoundaryGap = charts.True()
66+
opt.XAxis.FontStyle = charts.FontStyle{
67+
FontSize: 6.0,
68+
}
69+
opt.YAxis = []charts.YAxisOption{
70+
{
71+
Show: charts.False(), // disabling in favor of manually printed y-values
72+
Min: charts.FloatPointer(1.4),
73+
Max: charts.FloatPointer(8.0),
74+
LabelCount: 4,
75+
SpineLineShow: charts.True(),
76+
FontStyle: charts.FontStyle{
77+
FontSize: 8.0,
78+
},
79+
},
80+
}
81+
opt.Legend.Show = charts.False()
82+
opt.SymbolShow = charts.False()
83+
opt.StrokeWidth = 1.5
84+
85+
p, err := charts.NewPainter(charts.PainterOptions{
86+
OutputFormat: charts.ChartOutputPNG,
87+
// positions drawn below depend on the canvas size set here
88+
Width: 600,
89+
Height: 400,
90+
})
91+
if err != nil {
92+
panic(err)
93+
}
94+
if _, err = charts.NewLineChart(p, opt).Render(); err != nil {
95+
panic(err)
96+
}
97+
98+
// Custom drawing directly on the Painter
99+
fontStyle := charts.FontStyle{
100+
FontSize: 12,
101+
FontColor: drawing.ColorBlack,
102+
Font: charts.GetDefaultFont(),
103+
}
104+
p.SetFontStyle(fontStyle)
105+
//p.TextRotation("f/stop", 10, 170, chartdraw.DegreesToRadians(90))
106+
107+
fontStyle.FontColor = opt.Theme.GetSeriesColor(0)
108+
p.SetFontStyle(fontStyle)
109+
p.Text(labels[0], 420, 84)
110+
111+
fontStyle.FontColor = opt.Theme.GetSeriesColor(1)
112+
p.SetFontStyle(fontStyle)
113+
p.Text(labels[1], 45, 284)
114+
115+
fontStyle.FontColor = opt.Theme.GetSeriesColor(2)
116+
p.SetFontStyle(fontStyle)
117+
p.Text(labels[2], 140, 230)
118+
119+
fontStyle.FontColor = opt.Theme.GetSeriesColor(3)
120+
p.SetFontStyle(fontStyle)
121+
p.Text(labels[3], 160, 155)
122+
123+
fontStyle.FontSize = 8
124+
fontStyle.FontColor = opt.Theme.GetSeriesColor(0)
125+
p.SetFontStyle(fontStyle)
126+
p.Text("f/4.5", 42, 220)
127+
p.Text("f/5.0", 105, 196)
128+
p.Text("f/6.3", 370, 137)
129+
p.Text("f/7.1", 570, 100)
130+
131+
fontStyle.FontColor = opt.Theme.GetSeriesColor(1)
132+
p.SetFontStyle(fontStyle)
133+
p.Text("f/2.8", 5, 298)
134+
135+
fontStyle.FontColor = opt.Theme.GetSeriesColor(2)
136+
p.SetFontStyle(fontStyle)
137+
p.Text("f/4.0", 40, 244)
138+
139+
fontStyle.FontColor = opt.Theme.GetSeriesColor(3)
140+
p.SetFontStyle(fontStyle)
141+
p.Text("f/5.6", 92, 168)
142+
143+
if buf, err := p.Bytes(); err != nil {
144+
panic(err)
145+
} else if err = writeFile(buf); err != nil {
146+
panic(err)
147+
}
148+
}
149+
150+
func populateData() (values [][]float64, xAxisLabels []string, labels []string, err error) {
151+
for k, _ := range lensDefinitions {
152+
labels = append(labels, k)
153+
}
154+
sort.Slice(labels, func(i, j int) bool {
155+
return labels[i] < labels[j]
156+
})
157+
158+
for i := startMM; i <= endMM; i++ {
159+
xAxisLabels = append(xAxisLabels, fmt.Sprintf("%vmm", i))
160+
}
161+
162+
for _, lens := range labels {
163+
parts := strings.Split(lensDefinitions[lens], ",")
164+
count := (endMM - startMM) + 1
165+
lensValues := make([]float64, count)
166+
currentFValue := charts.GetNullValue()
167+
// for code simplicity we assume startMM is strictly BEFORE the first lens, this allows us to set null
168+
// values until the start point (which will be loaded on the first run of the loop)
169+
nextPartIndex := 0
170+
nextMM := startMM
171+
nextFStop := currentFValue
172+
for i := 0; i < count; i++ {
173+
if i+startMM == nextMM {
174+
currentFValue = nextFStop
175+
if nextPartIndex < len(parts) {
176+
nextFStop, nextMM, err = parseFStopMM(parts[nextPartIndex])
177+
nextPartIndex++
178+
if err != nil {
179+
return
180+
}
181+
} else {
182+
nextFStop = charts.GetNullValue()
183+
}
184+
}
185+
lensValues[i] = currentFValue
186+
}
187+
values = append(values, lensValues)
188+
}
189+
190+
return
191+
}
192+
193+
func parseFStopMM(str string) (float64, int, error) {
194+
parts := strings.Split(str, "f")
195+
if len(parts) != 2 {
196+
return 0, 0, fmt.Errorf("invalid lens spec str: '%s'", str)
197+
}
198+
mm, err := strconv.Atoi(parts[0])
199+
if err != nil {
200+
return 0, 0, err
201+
}
202+
if parts[1] == "-" {
203+
return charts.GetNullValue(), mm, nil
204+
}
205+
fstop, err := strconv.ParseFloat(parts[1], 64)
206+
return fstop, mm, err
207+
}

0 commit comments

Comments
 (0)