├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── chart.go ├── chart_test.go ├── examples ├── histogram │ └── main.go └── multi-chart │ └── main.go ├── sugar.go └── types └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | 8 | script: 9 | - go test 10 | - for d in examples/*; do echo $d; go run $d/main.go ; done 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Brent Pedersen - Bioinformatics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chartjs 2 | ------- 3 | 4 | go wrapper for [chartjs](http://chartjs.org) 5 | 6 | [![GoDoc](https://godoc.org/github.com/brentp/go-chartjs?status.png)](https://godoc.org/github.com/brentp/go-chartjs) 7 | [![Build Status](https://travis-ci.org/brentp/go-chartjs.svg)](https://travis-ci.org/brentp/go-chartjs) 8 | 9 | 10 | Chartjs charts are defined purely in JSON, so this library is mostly 11 | structs and struct-tags that dictate how to marshal to JSON. None of the currently 12 | implemented parts are stringly-typed in this library so it can avoid many errors. 13 | 14 | The chartjs javascript/JSON api has a [lot of surface area](http://www.chartjs.org/docs/). 15 | Currently, only the options that I use are provided. More can and will be added as I need 16 | them (or via pull-requests). 17 | 18 | There is a small amount of code to simplify creating charts. 19 | 20 | data to be plotted by chartjs has to meet this interface. 21 | ```Go 22 | type Values interface { 23 | // X-axis values. If only these are specified then it must be a Bar plot. 24 | Xs() []float64 25 | // Optional Y values. 26 | Ys() []float64 27 | // Rs are used to size points for chartType `Bubble`. If this returns an 28 | // empty slice then it's not used. 29 | Rs() []float64 30 | } 31 | ``` 32 | 33 | Example 34 | ------- 35 | 36 | This longish example shows common use of the library. 37 | 38 | ```Go 39 | package main 40 | 41 | import ( 42 | "log" 43 | "math" 44 | "os" 45 | 46 | chartjs "github.com/brentp/go-chartjs" 47 | ) 48 | 49 | // satisfy the required interface with this struct and methods. 50 | type xy struct { 51 | x []float64 52 | y []float64 53 | r []float64 54 | } 55 | 56 | func (v xy) Xs() []float64 { 57 | return v.x 58 | } 59 | func (v xy) Ys() []float64 { 60 | return v.y 61 | } 62 | func (v xy) Rs() []float64 { 63 | return v.r 64 | } 65 | 66 | func check(e error) { 67 | if e != nil { 68 | log.Fatal(e) 69 | } 70 | } 71 | 72 | func main() { 73 | var xys1 xy 74 | var xys2 xy 75 | 76 | // make some example data. 77 | for i := float64(0); i < 9; i += 0.1 { 78 | xys1.x = append(xys1.x, i) 79 | xys2.x = append(xys2.x, i) 80 | 81 | xys1.y = append(xys1.y, math.Sin(i)) 82 | xys2.y = append(xys2.y, 3*math.Cos(2*i)) 83 | 84 | } 85 | 86 | // a set of colors to work with. 87 | colors := []*types.RGBA{ 88 | &types.RGBA{102, 194, 165, 220}, 89 | &types.RGBA{250, 141, 98, 220}, 90 | &types.RGBA{141, 159, 202, 220}, 91 | &types.RGBA{230, 138, 195, 220}, 92 | } 93 | 94 | // a Dataset contains the data and styling info. 95 | d1 := chartjs.Dataset{Data: xys1, BorderColor: colors[1], Label: "sin(x)", Fill: chartjs.False, 96 | PointRadius: 10, PointBorderWidth: 4, BackgroundColor: colors[0]} 97 | 98 | d2 := chartjs.Dataset{Data: xys2, BorderWidth: 8, BorderColor: colors[3], Label: "3*cos(2*x)", 99 | Fill: chartjs.False, PointStyle: chartjs.Star} 100 | 101 | chart := chartjs.Chart{Label: "test-chart"} 102 | 103 | var err error 104 | _, err = chart.AddXAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Bottom, ScaleLabel: &chartjs.ScaleLabel{FontSize: 22, LabelString: "X", Display: chartjs.True}}) 105 | check(err) 106 | d1.YAxisID, err = chart.AddYAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Left, 107 | ScaleLabel: &chartjs.ScaleLabel{LabelString: "sin(x)", Display: chartjs.True}}) 108 | check(err) 109 | chart.AddDataset(d1) 110 | 111 | d2.YAxisID, err = chart.AddYAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Right, 112 | ScaleLabel: &chartjs.ScaleLabel{LabelString: "3*cos(2*x)", Display: chartjs.True}}) 113 | check(err) 114 | chart.AddDataset(d2) 115 | 116 | chart.Options.Responsive = chartjs.False 117 | 118 | wtr, err := os.Create("example-chartjs-multi.html") 119 | if err != nil { 120 | } 121 | if err := chart.SaveHTML(wtr, nil); err != nil { 122 | log.Fatal(err) 123 | } 124 | wtr.Close() 125 | } 126 | ``` 127 | 128 | The resulting html will have an interactive `` element that looks like this. 129 | 130 | ![plot](https://cloud.githubusercontent.com/assets/1739/20368217/5068a336-ac10-11e6-8d6c-f711c7c71df3.png "example plot") 131 | 132 | 133 | Live Examples 134 | ------------- 135 | 136 | [evaluating coverage on high throughput sequencing data](https://brentp.github.io/goleft/indexcov/ex-indexcov-roc.html) 137 | 138 | [inferring sex from sequencing coverage on X and Y chroms](https://brentp.github.io/goleft/indexcov/ex-indexcov-sex.html) 139 | -------------------------------------------------------------------------------- /chart.go: -------------------------------------------------------------------------------- 1 | // Package chartjs simplifies making chartjs.org plots in go. 2 | package chartjs 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "html/template" 9 | "math" 10 | 11 | "github.com/brentp/go-chartjs/types" 12 | ) 13 | 14 | var True = types.True 15 | var False = types.False 16 | 17 | var chartTypes = [...]string{ 18 | "line", 19 | "bar", 20 | "bubble", 21 | } 22 | 23 | type chartType int 24 | 25 | func (c chartType) MarshalJSON() ([]byte, error) { 26 | return []byte(`"` + chartTypes[c] + `"`), nil 27 | } 28 | 29 | const ( 30 | // Line is a "line" plot 31 | Line chartType = iota 32 | // Bar is a "bar" plot 33 | Bar 34 | // Bubble is a "bubble" plot 35 | Bubble 36 | ) 37 | 38 | type interpMode int 39 | 40 | const ( 41 | _ interpMode = iota 42 | InterpMonotone 43 | InterpDefault 44 | ) 45 | 46 | var interpModes = [...]string{ 47 | "", 48 | "monotone", 49 | "default", 50 | } 51 | 52 | func (m interpMode) MarshalJSON() ([]byte, error) { 53 | return []byte(`"` + interpModes[m] + `"`), nil 54 | } 55 | 56 | // XFloatFormat determines how many decimal places are sent in the JSON for X values. 57 | var XFloatFormat = "%.2f" 58 | 59 | // YFloatFormat determines how many decimal places are sent in the JSON for Y values. 60 | var YFloatFormat = "%.2f" 61 | 62 | // Values dictates the interface of data to be plotted. 63 | type Values interface { 64 | // X-axis values. If only these are specified then it must be a Bar plot. 65 | Xs() []float64 66 | // Optional Y values. 67 | Ys() []float64 68 | // Rs are used to size points for chartType `Bubble` 69 | Rs() []float64 70 | } 71 | 72 | func marshalValuesJSON(v Values, xformat, yformat string) ([]byte, error) { 73 | xs, ys, rs := v.Xs(), v.Ys(), v.Rs() 74 | if len(xs) == 0 { 75 | if len(rs) != 0 { 76 | return nil, fmt.Errorf("chart: bad format of Values data") 77 | } 78 | xs = ys[:len(ys)] 79 | ys = nil 80 | } 81 | buf := bytes.NewBuffer(make([]byte, 0, 8*len(xs))) 82 | buf.WriteRune('[') 83 | if len(rs) > 0 { 84 | if len(xs) != len(ys) || len(xs) != len(rs) { 85 | return nil, fmt.Errorf("chart: bad format of Values. All axes must be of the same length") 86 | } 87 | var err error 88 | for i, x := range xs { 89 | if i > 0 { 90 | buf.WriteRune(',') 91 | } 92 | y, r := ys[i], rs[i] 93 | if math.IsNaN(y) { 94 | _, err = buf.WriteString(fmt.Sprintf(("{\"x\":" + xformat + ",\"y\": null,\"r\":" + yformat + "}"), x, r)) 95 | } else { 96 | _, err = buf.WriteString(fmt.Sprintf(("{\"x\":" + xformat + ",\"y\":" + yformat + ",\"r\":" + yformat + "}"), x, y, r)) 97 | } 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | } else if len(ys) > 0 { 103 | if len(xs) != len(ys) { 104 | return nil, fmt.Errorf("chart: bad format of Values. X and Y must be of the same length") 105 | } 106 | var err error 107 | for i, x := range xs { 108 | if i > 0 { 109 | buf.WriteRune(',') 110 | } 111 | y := ys[i] 112 | if math.IsNaN(y) { 113 | _, err = buf.WriteString(fmt.Sprintf(("{\"x\":" + xformat + ",\"y\": null }"), x)) 114 | } else { 115 | _, err = buf.WriteString(fmt.Sprintf(("{\"x\":" + xformat + ",\"y\":" + yformat + "}"), x, y)) 116 | } 117 | if err != nil { 118 | return nil, err 119 | } 120 | } 121 | 122 | } else { 123 | for i, x := range xs { 124 | if i > 0 { 125 | buf.WriteRune(',') 126 | } 127 | _, err := buf.WriteString(fmt.Sprintf(xformat, x)) 128 | if err != nil { 129 | return nil, err 130 | } 131 | } 132 | } 133 | 134 | buf.WriteRune(']') 135 | return buf.Bytes(), nil 136 | } 137 | 138 | // shape indicates the type of marker used for plotting. 139 | type shape int 140 | 141 | var shapes = []string{ 142 | "", 143 | "circle", 144 | "triangle", 145 | "rect", 146 | "rectRot", 147 | "cross", 148 | "crossRot", 149 | "star", 150 | "line", 151 | "dash", 152 | } 153 | 154 | const ( 155 | empty = iota 156 | Circle 157 | Triangle 158 | Rect 159 | RectRot 160 | Cross 161 | CrossRot 162 | Star 163 | LinePoint 164 | Dash 165 | ) 166 | 167 | func (s shape) MarshalJSON() ([]byte, error) { 168 | return []byte(`"` + shapes[s] + `"`), nil 169 | } 170 | 171 | // Dataset wraps the "dataset" JSON 172 | type Dataset struct { 173 | Data Values `json:"-"` 174 | Type chartType `json:"type,omitempty"` 175 | BackgroundColor *types.RGBA `json:"backgroundColor,omitempty"` 176 | // BorderColor is the color of the line. 177 | BorderColor *types.RGBA `json:"borderColor,omitempty"` 178 | // BorderWidth is the width of the line. 179 | BorderWidth float64 `json:"borderWidth"` 180 | 181 | // Label indicates the name of the dataset to be shown in the legend. 182 | Label string `json:"label,omitempty"` 183 | Fill types.Bool `json:"fill,omitempty"` 184 | 185 | // SteppedLine of true means dont interpolate and ignore line tension. 186 | SteppedLine types.Bool `json:"steppedLine,omitempty"` 187 | LineTension float64 `json:"lineTension"` 188 | CubicInterpolationMode interpMode `json:"cubicInterpolationMode,omitempty"` 189 | PointBackgroundColor *types.RGBA `json:"pointBackgroundColor,omitempty"` 190 | PointBorderColor *types.RGBA `json:"pointBorderColor,omitempty"` 191 | PointBorderWidth float64 `json:"pointBorderWidth"` 192 | PointRadius float64 `json:"pointRadius"` 193 | PointHitRadius float64 `json:"pointHitRadius"` 194 | PointHoverRadius float64 `json:"pointHoverRadius"` 195 | PointHoverBorderColor *types.RGBA `json:"pointHoverBorderColor,omitempty"` 196 | PointHoverBorderWidth float64 `json:"pointHoverBorderWidth"` 197 | PointStyle shape `json:"pointStyle,omitempty"` 198 | 199 | ShowLine types.Bool `json:"showLine,omitempty"` 200 | SpanGaps types.Bool `json:"spanGaps,omitempty"` 201 | 202 | // Axis ID that matches the ID on the Axis where this dataset is to be drawn. 203 | XAxisID string `json:"xAxisID,omitempty"` 204 | YAxisID string `json:"yAxisID,omitempty"` 205 | 206 | // set the formatter for the data, e.g. "%.2f" 207 | // these are not exported in the json, just used to determine the decimals of precision to show 208 | XFloatFormat string `json:"-"` 209 | YFloatFormat string `json:"-"` 210 | } 211 | 212 | // MarshalJSON implements json.Marshaler interface. 213 | func (d Dataset) MarshalJSON() ([]byte, error) { 214 | xf, yf := d.XFloatFormat, d.YFloatFormat 215 | if xf == "" { 216 | xf = XFloatFormat 217 | } 218 | if yf == "" { 219 | yf = YFloatFormat 220 | } 221 | 222 | o, err := marshalValuesJSON(d.Data, xf, yf) 223 | // avoid recursion by creating an alias. 224 | type alias Dataset 225 | buf, err := json.Marshal(alias(d)) 226 | if err != nil { 227 | return nil, err 228 | } 229 | // replace '}' with ',' to continue struct 230 | if len(buf) > 0 { 231 | buf[len(buf)-1] = ',' 232 | } 233 | buf = append(buf, []byte(`"data":`)...) 234 | buf = append(buf, o...) 235 | buf = append(buf, '}') 236 | return buf, nil 237 | } 238 | 239 | // Data wraps the "data" JSON 240 | type Data struct { 241 | Datasets []Dataset `json:"datasets"` 242 | Labels []string `json:"labels"` 243 | } 244 | 245 | type axisType int 246 | 247 | var axisTypes = []string{ 248 | "category", 249 | "linear", 250 | "logarithmic", 251 | "time", 252 | "radialLinear", 253 | } 254 | 255 | const ( 256 | // Category is a categorical axis (this is the default), 257 | // used for bar plots. 258 | Category axisType = iota 259 | // Linear axis should be use for scatter plots. 260 | Linear 261 | // Log axis 262 | Log 263 | // Time axis 264 | Time 265 | // Radial axis 266 | Radial 267 | ) 268 | 269 | func (t axisType) MarshalJSON() ([]byte, error) { 270 | return []byte("\"" + axisTypes[t] + "\""), nil 271 | } 272 | 273 | type axisPosition int 274 | 275 | const ( 276 | // Bottom puts the axis on the bottom (used for Y-axis) 277 | Bottom axisPosition = iota + 1 278 | // Top puts the axis on the bottom (used for Y-axis) 279 | Top 280 | // Left puts the axis on the bottom (used for X-axis) 281 | Left 282 | // Right puts the axis on the bottom (used for X-axis) 283 | Right 284 | ) 285 | 286 | var axisPositions = []string{ 287 | "", 288 | "bottom", 289 | "top", 290 | "left", 291 | "right", 292 | } 293 | 294 | func (p axisPosition) MarshalJSON() ([]byte, error) { 295 | return []byte(`"` + axisPositions[p] + `"`), nil 296 | } 297 | 298 | // Axis corresponds to 'scale' in chart.js lingo. 299 | type Axis struct { 300 | Type axisType `json:"type"` 301 | Position axisPosition `json:"position,omitempty"` 302 | Label string `json:"label,omitempty"` 303 | ID string `json:"id,omitempty"` 304 | GridLines types.Bool `json:"gridLine,omitempty"` 305 | Stacked types.Bool `json:"stacked,omitempty"` 306 | 307 | // Bool differentiates between false and empty by use of pointer. 308 | Display types.Bool `json:"display,omitempty"` 309 | ScaleLabel *ScaleLabel `json:"scaleLabel,omitempty"` 310 | Tick *Tick `json:"ticks,omitempty"` 311 | } 312 | 313 | // Tick lets us set the range of the data. 314 | type Tick struct { 315 | Min float64 `json:"min,omitempty"` 316 | Max float64 `json:"max,omitempty"` 317 | BeginAtZero types.Bool `json:"beginAtZero,omitempty"` 318 | // TODO: add additional options from: tick options. 319 | } 320 | 321 | // ScaleLabel corresponds to scale title. 322 | // Display: True must be specified for this to be shown. 323 | type ScaleLabel struct { 324 | Display types.Bool `json:"display,omitempty"` 325 | LabelString string `json:"labelString,omitempty"` 326 | FontColor *types.RGBA `json:"fontColor,omitempty"` 327 | FontFamily string `json:"fontFamily,omitempty"` 328 | FontSize int `json:"fontSize,omitempty"` 329 | FontStyle string `json:"fontStyle,omitempty"` 330 | } 331 | 332 | // Axes holds the X and Y axies. Its simpler to use Chart.AddXAxis, Chart.AddYAxis. 333 | type Axes struct { 334 | XAxes []Axis `json:"xAxes,omitempty"` 335 | YAxes []Axis `json:"yAxes,omitempty"` 336 | } 337 | 338 | // AddX adds a X-Axis. 339 | func (a *Axes) AddX(x Axis) { 340 | a.XAxes = append(a.XAxes, x) 341 | } 342 | 343 | // AddY adds a Y-Axis. 344 | func (a *Axes) AddY(y Axis) { 345 | a.YAxes = append(a.YAxes, y) 346 | } 347 | 348 | // Option wraps the chartjs "option" 349 | type Option struct { 350 | Responsive types.Bool `json:"responsive,omitempty"` 351 | MaintainAspectRatio types.Bool `json:"maintainAspectRatio,omitempty"` 352 | Title *Title `json:"title,omitempty"` 353 | } 354 | 355 | // Title is the Options title 356 | type Title struct { 357 | Display types.Bool `json:"display,omitempty"` 358 | Text string `json:"text,omitempty"` 359 | } 360 | 361 | // Options wraps the chartjs "options" 362 | type Options struct { 363 | Option 364 | Scales Axes `json:"scales,omitempty"` 365 | Legend *Legend `json:"legend,omitempty"` 366 | Tooltip *Tooltip `json:"tooltips,omitempty"` 367 | } 368 | 369 | // Tooltip wraps chartjs "tooltips". 370 | // TODO: figure out how to make this work. 371 | type Tooltip struct { 372 | Enabled types.Bool `json:"enabled,omitempty"` 373 | Intersect types.Bool `json:"intersect,omitempty"` 374 | // TODO: make mode typed by Interaction modes. 375 | Mode string `json:"mode,omitempty"` 376 | Custom template.JSStr `json:"custom,omitempty"` 377 | } 378 | 379 | type Legend struct { 380 | Display types.Bool `json:"display,omitempty"` 381 | } 382 | 383 | // Chart is the top-level type from chartjs. 384 | type Chart struct { 385 | Type chartType `json:"type"` 386 | Label string `json:"label,omitempty"` 387 | Data Data `json:"data,omitempty"` 388 | Options Options `json:"options,omitempty"` 389 | } 390 | 391 | // AddDataset adds a dataset to the chart. 392 | func (c *Chart) AddDataset(d Dataset) { 393 | c.Data.Datasets = append(c.Data.Datasets, d) 394 | } 395 | 396 | // AddXAxis adds an x-axis to the chart and returns the ID of the added axis. 397 | func (c *Chart) AddXAxis(x Axis) (string, error) { 398 | if x.ID == "" { 399 | x.ID = fmt.Sprintf("xaxis%d", len(c.Options.Scales.XAxes)) 400 | } 401 | if x.Position == Left || x.Position == Right { 402 | return "", fmt.Errorf("chart: added x-axis to left or right") 403 | } 404 | c.Options.Scales.XAxes = append(c.Options.Scales.XAxes, x) 405 | return x.ID, nil 406 | } 407 | 408 | // AddYAxis adds an y-axis to the chart and return the ID of the added axis. 409 | func (c *Chart) AddYAxis(y Axis) (string, error) { 410 | if y.ID == "" { 411 | y.ID = fmt.Sprintf("yaxis%d", len(c.Options.Scales.YAxes)) 412 | } 413 | if y.Position == Top || y.Position == Bottom { 414 | return "", fmt.Errorf("chart: added y-axis to top or bottom") 415 | } 416 | c.Options.Scales.YAxes = append(c.Options.Scales.YAxes, y) 417 | return y.ID, nil 418 | } 419 | -------------------------------------------------------------------------------- /chart_test.go: -------------------------------------------------------------------------------- 1 | package chartjs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "os" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/brentp/go-chartjs/types" 12 | ) 13 | 14 | type xy struct { 15 | x []float64 16 | y []float64 17 | r []float64 18 | } 19 | 20 | func (v xy) Xs() []float64 { 21 | return v.x 22 | } 23 | func (v xy) Ys() []float64 { 24 | return v.y 25 | } 26 | func (v xy) Rs() []float64 { 27 | return v.r 28 | } 29 | 30 | func TestLine(t *testing.T) { 31 | 32 | //var axes Axes 33 | //axes.AddY(Axis{Type: Linear, Position: Bottom}) 34 | //axes.AddY(Axis{Type: Linear, Position: Left}) 35 | 36 | var xys xy 37 | for i := 0; i < 10; i++ { 38 | xys.x = append(xys.x, float64(i)) 39 | xys.y = append(xys.y, float64(i)) 40 | xys.r = append(xys.r, float64(i)) 41 | } 42 | 43 | d := Dataset{Data: xys, BackgroundColor: &types.RGBA{0, 255, 0, 200}, Label: "HHIHIHI"} 44 | 45 | //options := Options{Scales: axes} 46 | //options.Responsive = true 47 | //options.MaintainAspectRatio = false 48 | 49 | chart := Chart{Type: Bubble, Label: "test-chart"} 50 | //chart.Data = Data{Datasets: []Dataset{d}} 51 | chart.AddDataset(d) 52 | chart.AddXAxis(Axis{Type: Linear, Position: Bottom}) 53 | chart.AddYAxis(Axis{Type: Linear, Position: Right}) 54 | 55 | b, err := json.Marshal(chart) 56 | if err != nil { 57 | t.Fatalf("error marshaling chart: %+v", err) 58 | } 59 | fmt.Println(string(b)) 60 | } 61 | 62 | func TestBar(t *testing.T) { 63 | 64 | //var axes Axes 65 | //axes.AddY(Axis{Type: Linear, Position: Bottom}) 66 | //axes.AddY(Axis{Type: Linear, Position: Left}) 67 | 68 | var xs xy 69 | var labels []string 70 | for i := 0; i < 10; i++ { 71 | xs.x = append(xs.x, float64(i)) 72 | labels = append(labels, strconv.Itoa(i)) 73 | } 74 | d := Dataset{Data: xs, BackgroundColor: &types.RGBA{0, 255, 0, 200}} 75 | 76 | //options := Options{Scales: axes} 77 | //options.Responsive = true 78 | //options.MaintainAspectRatio = false 79 | 80 | chart := Chart{Type: Bar, Label: "test-chart"} 81 | //chart.Data = Data{Datasets: []Dataset{d}} 82 | chart.AddDataset(d) 83 | chart.Data.Labels = labels 84 | 85 | b, err := json.Marshal(chart) 86 | if err != nil { 87 | t.Fatalf("error marshaling chart: %+v", err) 88 | } 89 | fmt.Println(string(b)) 90 | } 91 | 92 | func TestHTML(t *testing.T) { 93 | 94 | var xys xy 95 | for i := float64(0); i < 9; i += 0.05 { 96 | xys.x = append(xys.x, float64(i)) 97 | xys.y = append(xys.y, math.Sin(float64(i))) 98 | xys.r = append(xys.r, float64(i)) 99 | } 100 | fmt.Println(len(xys.x)) 101 | 102 | d := Dataset{Data: xys, BackgroundColor: &types.RGBA{0, 255, 0, 200}, Label: "sin(x)"} 103 | 104 | //options := Options{Scales: axes} 105 | //options.Responsive = true 106 | //options.MaintainAspectRatio = false 107 | 108 | chart := Chart{Type: Bubble, Label: "test-chart"} 109 | //chart.Data = Data{Datasets: []Dataset{d}} 110 | chart.AddDataset(d) 111 | chart.AddXAxis(Axis{Type: Linear, Position: Bottom}) 112 | chart.AddYAxis(Axis{Type: Linear, Position: Right}) 113 | chart.Options.Responsive = types.False 114 | 115 | wtr, err := os.Create("test-chartjs.html") 116 | if err != nil { 117 | t.Fatalf("error opening file: %+v", err) 118 | } 119 | if err := chart.SaveHTML(wtr, nil); err != nil { 120 | t.Fatalf("error saving chart: %+v", err) 121 | } 122 | wtr.Close() 123 | } 124 | 125 | func TestMultipleCharts(t *testing.T) { 126 | var xys1 xy 127 | var xys2 xy 128 | 129 | for i := float64(0); i < 9; i += 0.1 { 130 | xys1.x = append(xys1.x, float64(i)) 131 | xys2.x = append(xys2.x, float64(i)) 132 | 133 | xys1.y = append(xys1.y, math.Sin(float64(i))) 134 | 135 | xys2.y = append(xys2.y, 2*math.Cos(float64(i))) 136 | 137 | } 138 | 139 | // a set of colors to work with. 140 | colors := []*types.RGBA{ 141 | &types.RGBA{102, 194, 165, 220}, 142 | &types.RGBA{250, 141, 98, 220}, 143 | &types.RGBA{141, 159, 202, 220}, 144 | &types.RGBA{230, 138, 195, 220}, 145 | } 146 | 147 | d1 := Dataset{Data: xys1, BorderColor: colors[0], Label: "sin(x)", Fill: types.False, 148 | PointRadius: 10, PointBorderWidth: 4, BackgroundColor: colors[1]} 149 | 150 | d2 := Dataset{Data: xys2, BorderWidth: 8, BorderColor: colors[2], Label: "2 * cos(x)", Fill: types.False} 151 | 152 | chart := Chart{Type: Line, Label: "test-chart"} 153 | chart.AddXAxis(Axis{Type: Linear, Position: Bottom, ScaleLabel: &ScaleLabel{FontSize: 22, LabelString: "X", Display: types.True}}) 154 | var err error 155 | d1.YAxisID, err = chart.AddYAxis(Axis{Type: Linear, Position: Left, ScaleLabel: &ScaleLabel{LabelString: "sin(x)", Display: types.True}}) 156 | if err != nil { 157 | t.Fatalf("error adding axis: %s", err) 158 | } 159 | d2.YAxisID, err = chart.AddYAxis(Axis{Type: Linear, Position: Right, ScaleLabel: &ScaleLabel{LabelString: "2 * cos(x)", Display: types.True}}) 160 | if err != nil { 161 | t.Fatalf("error adding axis: %s", err) 162 | } 163 | 164 | chart.AddDataset(d2) 165 | chart.AddDataset(d1) 166 | 167 | chart.Options.Responsive = types.False 168 | 169 | wtr, err := os.Create("test-chartjs-multi.html") 170 | if err != nil { 171 | t.Fatalf("error opening file: %+v", err) 172 | } 173 | if err := chart.SaveHTML(wtr, nil); err != nil { 174 | t.Fatalf("error saving chart: %+v", err) 175 | } 176 | wtr.Close() 177 | } 178 | 179 | func TestAnno(t *testing.T) { 180 | 181 | var xys xy 182 | for i := float64(0); i < 9; i += 0.05 { 183 | xys.x = append(xys.x, float64(i)) 184 | xys.y = append(xys.y, math.Sin(float64(i))) 185 | xys.r = append(xys.r, float64(i)) 186 | } 187 | 188 | d := Dataset{Data: xys, BackgroundColor: &types.RGBA{0, 255, 0, 200}, Label: "sin(x)"} 189 | 190 | //options := Options{Scales: axes} 191 | //options.Responsive = true 192 | //options.MaintainAspectRatio = false 193 | 194 | chart := Chart{Type: Bubble, Label: "test-chart"} 195 | //chart.Data = Data{Datasets: []Dataset{d}} 196 | chart.AddDataset(d) 197 | chart.AddXAxis(Axis{Type: Linear, Position: Bottom}) 198 | chart.AddYAxis(Axis{Type: Linear, Position: Right}) 199 | chart.Options.Responsive = types.False 200 | 201 | wtr, err := os.Create("test-chart-anno.html") 202 | if err != nil { 203 | t.Fatalf("error opening file: %+v", err) 204 | } 205 | if err := chart.SaveHTML(wtr, nil); err != nil { 206 | t.Fatalf("error saving chart: %+v", err) 207 | } 208 | wtr.Close() 209 | } 210 | -------------------------------------------------------------------------------- /examples/histogram/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math" 9 | "os" 10 | "strconv" 11 | 12 | chartjs "github.com/brentp/go-chartjs" 13 | "github.com/brentp/go-chartjs/types" 14 | "github.com/gonum/floats" 15 | "github.com/pkg/browser" 16 | ) 17 | 18 | type hister struct { 19 | vals []float64 20 | bins []int 21 | binNames []string 22 | N int 23 | Min float64 24 | Max float64 25 | } 26 | 27 | func (h *hister) hist() { 28 | if h.N <= 0 { 29 | h.N = int(0.5 + math.Pow(floats.Sum(h.vals), 0.33)) 30 | } 31 | log.Println(h.N) 32 | if h.N < 1 { 33 | h.N = 1 34 | } 35 | if h.Min == 0 { 36 | h.Min = floats.Min(h.vals) 37 | } 38 | if h.Max == 0 { 39 | h.Max = floats.Max(h.vals) 40 | } 41 | 42 | w := (h.Max - h.Min) / float64(h.N) 43 | bins := make([]int, h.N) 44 | h.binNames = make([]string, h.N) 45 | for i := range h.binNames { 46 | v := h.Min + float64(i)*w 47 | if w < 2 { 48 | h.binNames[i] = fmt.Sprintf("%.2f-%.2f", v, v+w) 49 | } else { 50 | h.binNames[i] = fmt.Sprintf("%.0f-%.0f", v, v+w) 51 | } 52 | } 53 | 54 | for _, v := range h.vals { 55 | if v < h.Min || v > h.Max { 56 | continue 57 | } 58 | b := int((v - h.Min) / w) 59 | if v == h.Max { 60 | b = h.N - 1 61 | } 62 | bins[b]++ 63 | } 64 | h.bins = bins 65 | } 66 | 67 | func (h hister) Xs() []float64 { 68 | bins := make([]float64, len(h.bins)) 69 | for i, b := range h.bins { 70 | bins[i] = float64(b) 71 | } 72 | return bins 73 | } 74 | 75 | func (h hister) Ys() []float64 { 76 | return nil 77 | } 78 | func (h hister) Rs() []float64 { 79 | return nil 80 | } 81 | 82 | // IsStdin checks if we are getting data from stdin. 83 | func isStdin() bool { 84 | // http://stackoverflow.com/a/26567513 85 | stat, err := os.Stdin.Stat() 86 | if err != nil { 87 | return false 88 | } 89 | return (stat.Mode() & os.ModeCharDevice) == 0 90 | } 91 | 92 | func main() { 93 | if !isStdin() { 94 | fmt.Fprintln(os.Stderr, "expecting values on stdin") 95 | os.Exit(0) 96 | } 97 | 98 | vals := make([]float64, 0, 100) 99 | stdin := bufio.NewReader(os.Stdin) 100 | 101 | for { 102 | line, err := stdin.ReadString('\n') 103 | if err == io.EOF { 104 | break 105 | } 106 | if err != nil { 107 | panic(err) 108 | } 109 | v, err := strconv.ParseFloat(line[:len(line)-1], 64) 110 | if err != nil { 111 | panic(err) 112 | } 113 | vals = append(vals, v) 114 | } 115 | 116 | h := hister{vals: vals} 117 | h.N = 16 118 | h.hist() 119 | 120 | chart := &chartjs.Chart{} 121 | d := chartjs.Dataset{Data: h, Type: chartjs.Bar} 122 | d.BackgroundColor = &types.RGBA{102, 194, 165, 220} 123 | d.BorderWidth = 2 124 | d.Fill = types.True 125 | 126 | yax := chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Left} 127 | yax.ScaleLabel = &chartjs.ScaleLabel{Display: types.True, LabelString: "Count"} 128 | _, err := chart.AddYAxis(yax) 129 | check(err) 130 | chart.Options.Scales.YAxes[0].Tick = &chartjs.Tick{BeginAtZero: types.True} 131 | 132 | chart.Data.Labels = h.binNames 133 | chart.Type = chartjs.Bar 134 | chart.AddDataset(d) 135 | 136 | chart.Options.Responsive = chartjs.False 137 | chart.Options.Legend = &chartjs.Legend{Display: chartjs.False} 138 | 139 | wtr, err := os.Create("hist.html") 140 | 141 | check(err) 142 | if err := chart.SaveHTML(wtr, nil); err != nil { 143 | log.Fatal(err) 144 | } 145 | wtr.Close() 146 | browser.OpenFile("hist.html") 147 | 148 | } 149 | 150 | func check(e error) { 151 | if e != nil { 152 | panic(e) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /examples/multi-chart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math" 6 | "os" 7 | 8 | chartjs "github.com/brentp/go-chartjs" 9 | "github.com/brentp/go-chartjs/types" 10 | ) 11 | 12 | type xy struct { 13 | x []float64 14 | y []float64 15 | r []float64 16 | } 17 | 18 | func (v xy) Xs() []float64 { 19 | return v.x 20 | } 21 | func (v xy) Ys() []float64 { 22 | return v.y 23 | } 24 | func (v xy) Rs() []float64 { 25 | return v.r 26 | } 27 | 28 | func check(e error) { 29 | if e != nil { 30 | log.Fatal(e) 31 | } 32 | } 33 | 34 | func main() { 35 | var xys1 xy 36 | var xys2 xy 37 | 38 | for i := float64(0); i < 9; i += 0.1 { 39 | xys1.x = append(xys1.x, i) 40 | xys2.x = append(xys2.x, i) 41 | 42 | xys1.y = append(xys1.y, math.Sin(i)) 43 | xys2.y = append(xys2.y, 3*math.Cos(2*i)) 44 | 45 | } 46 | 47 | // a set of colors to work with. 48 | colors := []*types.RGBA{ 49 | &types.RGBA{102, 194, 165, 220}, 50 | &types.RGBA{250, 141, 98, 220}, 51 | &types.RGBA{141, 159, 202, 220}, 52 | &types.RGBA{230, 138, 195, 220}, 53 | } 54 | 55 | // a Dataset contains the data and styling info. 56 | d1 := chartjs.Dataset{Data: xys1, BorderColor: colors[1], Label: "sin(x)", Fill: chartjs.False, 57 | PointRadius: 10, PointBorderWidth: 4, BackgroundColor: colors[0]} 58 | 59 | d2 := chartjs.Dataset{Data: xys2, BorderWidth: 8, BorderColor: colors[3], Label: "3*cos(2*x)", 60 | Fill: chartjs.False, PointStyle: chartjs.Star} 61 | 62 | chart := chartjs.Chart{Label: "test-chart"} 63 | 64 | var err error 65 | _, err = chart.AddXAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Bottom, ScaleLabel: &chartjs.ScaleLabel{FontSize: 22, LabelString: "X", Display: chartjs.True}}) 66 | check(err) 67 | d1.YAxisID, err = chart.AddYAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Left, 68 | ScaleLabel: &chartjs.ScaleLabel{LabelString: "sin(x)", Display: chartjs.True}}) 69 | check(err) 70 | chart.AddDataset(d1) 71 | 72 | d2.YAxisID, err = chart.AddYAxis(chartjs.Axis{Type: chartjs.Linear, Position: chartjs.Right, 73 | ScaleLabel: &chartjs.ScaleLabel{LabelString: "3*cos(2*x)", Display: chartjs.True}}) 74 | check(err) 75 | chart.AddDataset(d2) 76 | 77 | chart.Options.Responsive = chartjs.False 78 | 79 | wtr, err := os.Create("example-chartjs-multi.html") 80 | if err != nil { 81 | } 82 | if err := chart.SaveHTML(wtr, nil); err != nil { 83 | log.Fatal(err) 84 | } 85 | wtr.Close() 86 | } 87 | -------------------------------------------------------------------------------- /sugar.go: -------------------------------------------------------------------------------- 1 | package chartjs 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "io" 7 | ) 8 | 9 | // this file implements some syntactic sugar for creating charts 10 | 11 | // JQuery holds the path to hosted JQuery 12 | var JQuery = "https://code.jquery.com/jquery-2.2.4.min.js" 13 | 14 | // ChartJS holds the path to hosted ChartJS 15 | var ChartJS = "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.6.0/Chart.bundle.js" 16 | 17 | const tmpl = ` 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | {{ $height := index . "height" }} 28 | {{ $width := index . "width" }} 29 | {{ range $i, $json := index . "charts" }} 30 | 31 |
32 | {{ end }} 33 | {{ index . "customHTML" }} 34 | 35 | 46 | ` 47 | 48 | // SaveCharts writes the charts and the required HTML to an io.Writer 49 | func SaveCharts(w io.Writer, tmap map[string]interface{}, charts ...Chart) error { 50 | if tmap == nil { 51 | tmap = make(map[string]interface{}) 52 | } 53 | 54 | if _, ok := tmap["height"]; !ok { 55 | tmap["height"] = 400 56 | } 57 | if _, ok := tmap["width"]; !ok { 58 | tmap["width"] = 400 59 | } 60 | jscharts := make([]template.JS, 0, len(charts)) 61 | for _, c := range charts { 62 | cjson, err := json.Marshal(c) 63 | if err != nil { 64 | return err 65 | } 66 | jscharts = append(jscharts, template.JS(cjson)) 67 | } 68 | for k, v := range tmap { 69 | if chart, ok := v.(Chart); ok { 70 | cjson, err := json.Marshal(chart) 71 | if err != nil { 72 | return err 73 | } 74 | tmap[k] = template.JS(cjson) 75 | } 76 | } 77 | 78 | tmap["charts"] = jscharts 79 | if _, ok := tmap["JQuery"]; !ok { 80 | tmap["JQuery"] = JQuery 81 | } 82 | if _, ok := tmap["ChartJS"]; !ok { 83 | tmap["ChartJS"] = ChartJS 84 | } 85 | if _, ok := tmap["custom"]; !ok { 86 | tmap["custom"] = "" 87 | } 88 | if _, ok := tmap["customHTML"]; !ok { 89 | tmap["customHTML"] = "" 90 | } 91 | if _, ok := tmap["template"]; !ok { 92 | tmap["template"] = tmpl 93 | } 94 | t, err := template.New("chartjs").Parse(tmap["template"].(string)) 95 | if err != nil { 96 | return err 97 | } 98 | return t.Execute(w, tmap) 99 | } 100 | 101 | // SaveHTML writes the chart and minimal HTML to an io.Writer. 102 | func (c Chart) SaveHTML(w io.Writer, tmap map[string]interface{}) error { 103 | return SaveCharts(w, tmap, c) 104 | } 105 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | ) 7 | 8 | // RGBA amends image/color.RGBA to have a MarshalJSON that meets the expectations of chartjs. 9 | type RGBA color.RGBA 10 | 11 | // MarshalJSON satisfies the json.Marshaler interface. 12 | func (c RGBA) MarshalJSON() ([]byte, error) { 13 | return []byte(fmt.Sprintf("\"rgba(%d, %d, %d, %.3f)\"", c.R, c.G, c.B, float64(c.A)/255)), nil 14 | } 15 | 16 | // Bool is a convenience typedef for pointer to bool so that we can differentiate between unset 17 | // and false. 18 | type Bool *bool 19 | 20 | var ( 21 | t = true 22 | f = false 23 | // True is a convenience for pointer to true 24 | True = Bool(&t) 25 | 26 | // False is a convenience for pointer to false 27 | False = Bool(&f) 28 | ) 29 | --------------------------------------------------------------------------------