Skip to content

Commit d0adbe3

Browse files
Add animation slider example
AND some missing utility functions
1 parent dbbe562 commit d0adbe3

File tree

4 files changed

+405
-1
lines changed

4 files changed

+405
-1
lines changed

examples/animation_slider/main.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"sort"
7+
8+
grob "github.com/MetalBlueberry/go-plotly/generated/v2.19.0/graph_objects"
9+
"github.com/MetalBlueberry/go-plotly/pkg/offline"
10+
"github.com/MetalBlueberry/go-plotly/pkg/types"
11+
"github.com/go-gota/gota/dataframe"
12+
"golang.org/x/exp/constraints"
13+
)
14+
15+
// https://plotly.com/javascript/gapminder-example/
16+
17+
func readCSVData() dataframe.DataFrame {
18+
// country,year,pop,continent,lifeExp,gdpPercap
19+
response, err := http.Get("https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv")
20+
if err != nil {
21+
log.Fatalf("Unable to fetch csv data, %s", err)
22+
}
23+
defer response.Body.Close()
24+
25+
df := dataframe.ReadCSV(response.Body)
26+
27+
if err != nil {
28+
log.Fatalf("Unable to import CSV data, %s", err)
29+
}
30+
return df
31+
}
32+
33+
func main() {
34+
df := readCSVData()
35+
36+
country := df.Col("country")
37+
year := df.Col("year")
38+
population := df.Col("pop")
39+
continent := df.Col("continent")
40+
lifeExp := df.Col("lifeExp")
41+
gdpPercap := df.Col("gdpPercap")
42+
43+
continentClassifications, continentKey := split(continent.Records(), [][]string{
44+
year.Records(),
45+
country.Records(),
46+
population.Records(),
47+
lifeExp.Records(),
48+
gdpPercap.Records(),
49+
})
50+
51+
indexYearContinent := make(map[string]map[string][][]string)
52+
for _, continent := range continentKey {
53+
continentClassification := continentClassifications[continent]
54+
year := continentClassification[0]
55+
yearClassification, yearKeys := split(year, continentClassification[1:])
56+
for _, year := range yearKeys {
57+
if indexYearContinent[year] == nil {
58+
indexYearContinent[year] = make(map[string][][]string)
59+
}
60+
indexYearContinent[year][continent] = yearClassification[year]
61+
}
62+
}
63+
64+
frames := []grob.Frame{}
65+
sliderSteps := []grob.LayoutSliderStep{}
66+
67+
years := SortedKeys(indexYearContinent)
68+
69+
for _, year := range years {
70+
indexContinent := indexYearContinent[year]
71+
data := []types.Trace{}
72+
73+
continents := SortedKeys(indexContinent)
74+
for _, continent := range continents {
75+
records := indexContinent[continent]
76+
data = append(data, &grob.Scatter{
77+
Name: types.S(continent),
78+
X: types.DataArray(records[2]), // life expectancy
79+
Y: types.DataArray(records[3]), // gdp per capita
80+
Ids: types.DataArray(records[0]), // country
81+
Text: types.ArrayOKArray(types.SA(records[0])...), // country
82+
Marker: &grob.ScatterMarker{
83+
// Sizemode: grob.ScatterMarkerSizemodeArea,
84+
Size: types.ArrayOKArray(types.NSA(records[1])...), // population
85+
// Sizeref: types.N(200000),
86+
},
87+
})
88+
}
89+
90+
frameName := types.S(year)
91+
frames = append(frames, grob.Frame{
92+
Name: frameName,
93+
Data: data,
94+
})
95+
96+
sliderSteps = append(sliderSteps, grob.LayoutSliderStep{
97+
Method: grob.LayoutSliderStepMethodAnimate,
98+
Label: frameName,
99+
Args: []interface{}{
100+
[]interface{}{frameName},
101+
&ButtonArgs{
102+
Mode: "immediate",
103+
Transition: map[string]interface{}{"duration": 300},
104+
Frame: map[string]interface{}{"duration": 300, "redraw": false},
105+
},
106+
},
107+
})
108+
}
109+
110+
indexContinent := indexYearContinent[years[0]]
111+
data := []types.Trace{}
112+
113+
continents := SortedKeys(indexContinent)
114+
for _, continent := range continents {
115+
records := indexContinent[continent]
116+
data = append(data, &grob.Scatter{
117+
Name: types.S(continent),
118+
X: types.DataArray(records[2]), // life expectancy
119+
Y: types.DataArray(records[3]), // gdp per capita
120+
Ids: types.DataArray(records[0]), // country
121+
Text: types.ArrayOKArray(types.SA(records[0])...), // country
122+
Mode: grob.ScatterModeMarkers,
123+
Marker: &grob.ScatterMarker{
124+
Sizemode: grob.ScatterMarkerSizemodeArea,
125+
Size: types.ArrayOKArray(types.NSA(records[1])...), // population
126+
Sizeref: types.N(200000),
127+
},
128+
})
129+
}
130+
131+
fig := &grob.Fig{
132+
Data: data,
133+
Layout: &grob.Layout{
134+
Xaxis: &grob.LayoutXaxis{
135+
Title: &grob.LayoutXaxisTitle{
136+
Text: "Life Expectancy",
137+
},
138+
Range: []int{30, 85},
139+
},
140+
Yaxis: &grob.LayoutYaxis{
141+
Title: &grob.LayoutYaxisTitle{
142+
Text: "GDP per Capita",
143+
},
144+
Type: grob.LayoutYaxisTypeLog,
145+
},
146+
Hovermode: grob.LayoutHovermodeClosest,
147+
Updatemenus: []grob.LayoutUpdatemenu{
148+
{
149+
X: types.N(0),
150+
Y: types.N(0),
151+
Xanchor: grob.LayoutUpdatemenuXanchorLeft,
152+
Yanchor: grob.LayoutUpdatemenuYanchorTop,
153+
Showactive: types.False,
154+
Direction: grob.LayoutUpdatemenuDirectionLeft,
155+
Type: grob.LayoutUpdatemenuTypeButtons,
156+
Pad: &grob.LayoutUpdatemenuPad{
157+
T: types.N(87),
158+
R: types.N(10),
159+
},
160+
Buttons: []grob.LayoutUpdatemenuButton{
161+
{
162+
Label: types.S("Play"),
163+
Method: grob.LayoutUpdatemenuButtonMethodAnimate,
164+
Args: []*ButtonArgs{
165+
nil,
166+
{
167+
Mode: "immediate",
168+
FromCurrent: true,
169+
Transition: map[string]interface{}{"duration": 300},
170+
Frame: map[string]interface{}{"duration": 500, "redraw": false},
171+
},
172+
},
173+
},
174+
{
175+
Label: types.S("Pause"),
176+
Method: grob.LayoutUpdatemenuButtonMethodAnimate,
177+
Args: []interface{}{
178+
[]interface{}{nil},
179+
&ButtonArgs{
180+
Mode: "immediate",
181+
FromCurrent: true,
182+
Transition: map[string]interface{}{"duration": 0},
183+
Frame: map[string]interface{}{"duration": 0, "redraw": false},
184+
},
185+
},
186+
},
187+
},
188+
},
189+
},
190+
Sliders: []grob.LayoutSlider{
191+
{
192+
Pad: &grob.LayoutSliderPad{
193+
L: types.N(130),
194+
T: types.N(55),
195+
},
196+
Currentvalue: &grob.LayoutSliderCurrentvalue{
197+
Visible: types.True,
198+
Prefix: types.S("Year:"),
199+
Xanchor: grob.LayoutSliderCurrentvalueXanchorRight,
200+
Font: &grob.LayoutSliderCurrentvalueFont{
201+
Size: types.N(20),
202+
},
203+
},
204+
Steps: sliderSteps,
205+
},
206+
},
207+
},
208+
Frames: frames,
209+
Animation: &grob.Animation{
210+
Transition: &grob.AnimationTransition{
211+
Duration: types.N(500),
212+
Easing: grob.AnimationTransitionEasingCubicInOut,
213+
},
214+
Frame: &grob.AnimationFrame{
215+
Duration: types.N(500),
216+
Redraw: types.True,
217+
},
218+
},
219+
}
220+
221+
offline.Serve(fig)
222+
}
223+
224+
type ButtonArgs struct {
225+
Frame map[string]interface{} `json:"frame,omitempty"`
226+
Transition map[string]interface{} `json:"transition,omitempty"`
227+
FromCurrent bool `json:"fromcurrent,omitempty"`
228+
Mode string `json:"mode,omitempty"`
229+
}
230+
231+
// given a reference slice, it will split the other slices in the same way
232+
// so if reface is ["a","b","a","b"] and slices is [[1,2,3,4],["s1","s2","s3","s4"]]
233+
// it will return
234+
// {
235+
// "a":[[1,3],["s1","s3"]],
236+
// "b":[[2,4],["s2","s4"]]
237+
// }
238+
func split[T constraints.Ordered, Y any](reference []T, slices [][]Y) (map[T][][]Y, []T) {
239+
indices, keys := findIndices(reference)
240+
241+
result := map[T][][]Y{}
242+
for i, slice := range slices {
243+
sections := splitByIndices(slice, indices)
244+
for j, key := range keys {
245+
if result[key] == nil {
246+
result[key] = make([][]Y, len(slices))
247+
}
248+
result[key][i] = sections[j]
249+
}
250+
}
251+
return result, keys
252+
}
253+
254+
// given an slice, it will return the indices and the keys you can use to classify it by its types
255+
// so ["a","b","a","b"] will return [[0,2],[1,3]] and ["a","b"]
256+
func findIndices[T constraints.Ordered](input []T) ([][]int, []T) {
257+
indexMap := make(map[T][]int)
258+
var keys []T
259+
260+
// Populate the map with indices grouped by the value
261+
for i, val := range input {
262+
if _, found := indexMap[val]; !found {
263+
keys = append(keys, val)
264+
}
265+
indexMap[val] = append(indexMap[val], i)
266+
}
267+
268+
// Collect the grouped indices into a result slice in the order of first appearance
269+
var result [][]int
270+
for _, key := range keys {
271+
result = append(result, indexMap[key])
272+
}
273+
274+
return result, keys
275+
}
276+
277+
// given a slice, it will classify it by the given indices.
278+
// so ["a","b","c","d"] with [[0,2],[1,3]] will return [["a","c"],["b","d"]]
279+
func splitByIndices[T any](orginal []T, indices [][]int) [][]T {
280+
result := [][]T{}
281+
for i, section := range indices {
282+
result = append(result, []T{})
283+
for _, value := range section {
284+
result[i] = append(result[i], orginal[value])
285+
}
286+
}
287+
288+
return result
289+
}
290+
291+
func SortedKeys[T any](m map[string]T) []string {
292+
keys := make([]string, 0, len(m))
293+
for key := range m {
294+
keys = append(keys, key)
295+
}
296+
sort.Strings(keys)
297+
return keys
298+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestSplit(t *testing.T) {
9+
// Test case 1: Basic functionality
10+
reference := []string{"a", "b", "a", "b"}
11+
slices := [][]interface{}{
12+
{1, 2, 3, 4},
13+
{"s1", "s2", "s3", "s4"},
14+
}
15+
expected := map[string][][]interface{}{
16+
"a": {
17+
{1, 3},
18+
{"s1", "s3"},
19+
},
20+
"b": {
21+
{2, 4},
22+
{"s2", "s4"},
23+
},
24+
}
25+
26+
result, _ := split(reference, slices)
27+
28+
if !reflect.DeepEqual(result, expected) {
29+
t.Errorf("Test case 1 failed. Expected %v, got %v", expected, result)
30+
}
31+
32+
// Test case 2: Single element in reference
33+
reference = []string{"a"}
34+
slices = [][]interface{}{
35+
{5},
36+
{"single"},
37+
}
38+
expected = map[string][][]interface{}{
39+
"a": {
40+
{5},
41+
{"single"},
42+
},
43+
}
44+
45+
result, _ = split(reference, slices)
46+
47+
if !reflect.DeepEqual(result, expected) {
48+
t.Errorf("Test case 2 failed. Expected %v, got %v", expected, result)
49+
}
50+
51+
// Test case 3: Empty slices
52+
reference = []string{}
53+
slices = [][]interface{}{}
54+
expected = map[string][][]interface{}{}
55+
56+
result, _ = split(reference, slices)
57+
58+
if !reflect.DeepEqual(result, expected) {
59+
t.Errorf("Test case 3 failed. Expected %v, got %v", expected, result)
60+
}
61+
62+
// Test case 4: Multiple same keys
63+
reference = []string{"x", "x", "y", "y"}
64+
slices = [][]interface{}{
65+
{10, 20, 30, 40},
66+
{"a", "b", "c", "d"},
67+
}
68+
expected = map[string][][]interface{}{
69+
"x": {
70+
{10, 20},
71+
{"a", "b"},
72+
},
73+
"y": {
74+
{30, 40},
75+
{"c", "d"},
76+
},
77+
}
78+
79+
result, _ = split(reference, slices)
80+
81+
if !reflect.DeepEqual(result, expected) {
82+
t.Errorf("Test case 4 failed. Expected %v, got %v", expected, result)
83+
}
84+
}

examples/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ require (
77
github.com/go-gota/gota v0.12.0
88
github.com/lucasb-eyer/go-colorful v1.2.0
99
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
10+
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
1011
)
1112

1213
require (
13-
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
1414
golang.org/x/net v0.28.0 // indirect
1515
golang.org/x/sys v0.23.0 // indirect
1616
gonum.org/v1/gonum v0.15.0 // indirect

0 commit comments

Comments
 (0)