diff --git a/README.md b/README.md index 32b90ab..11f2710 100644 --- a/README.md +++ b/README.md @@ -17,178 +17,68 @@ To install `chart` run the following: Most of the components are interchangeable so feel free to crib whatever you want. -# Usage +# Ouptut Examples - ![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/goog_ltm.png) +Spark Lines: +![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/tvix_ltm.png) -The chart code to produce the above is as follows: +Single axis: -```go -// note this assumes that xvalues and yvalues -// have been pulled from a pricing service. -graph := chart.Chart{ - Width: 1024, - Height: 400, - YAxis: chart.YAxis { - Style: chart.Style{ - Show: true, - }, - }, - XAxis: chart.XAxis { - Style: chart.Style{ - Show: true, - }, - }, - Series: []chart.Series{ - chart.TimeSeries{ - XValues: xvalues, - YValues: yvalues, - Style: chart.Style { - FillColor: chart.DefaultSeriesStrokeColors[0].WithAlpha(64), - }, - }, - chart.AnnotationSeries{ - Name: "Last Value", - Style: chart.Style{ - Show: true, - StrokeColor: chart.DefaultSeriesStrokeColors[0], - }, - Annotations: []chart.Annotation{ - chart.Annotation{ - X: chart.TimeToFloat64(xvalues[len(xvalues)-1]), - Y: yvalues[len(yvalues)-1], - Label: chart.FloatValueFormatter(yvalues[len(yvalues)-1]), - }, - }, - }, - }, -} -graph.Render(chart.PNG, buffer) //thats it! -``` +![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/goog_ltm.png) -The key areas to note are that we have to explicitly turn on two features, the axes and add the last value label annotation series. When calling `.Render(..)` we add a parameter, `chart.PNG` that tells the renderer to use a raster renderer. Another option is to use `chart.SVG` which will use the vector renderer and create an svg representation of the chart. +Two axis: -# Alternate Usage +![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/two_axis.png) -You can alternately leave a bunch of features turned off and constrain the proportions to something like a spark line: +Simple Moving Average: - ![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/tvix_ltm.png) +![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/ma_goog_ltm.png) -The code to produce the above would be: - -```go -// note this assumes that xvalues and yvalues -// have been pulled from a pricing service. -graph := chart.Chart{ - Width: 1024, - Height: 100, - Series: []chart.Series{ - chart.TimeSeries{ - XValues: xvalues, - YValues: yvalues, - }, - }, -} -graph.Render(chart.PNG, buffer) -``` - -# 2 Y-Axis Charts - - ![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/two_axis.png) - -It is also possible to draw series against 2 separate y-axis with their own ranges (usually good for comparison charts). -In order to map the series to an alternate axis make sure to set the `YAxis` property of the series to `YAxisSecondary`. - -```go -graph := chart.Chart{ - Title: stock.Name, - TitleStyle: chart.Style{ - Show: false, - }, - Width: width, - Height: height, - XAxis: chart.XAxis{ - Style: chart.Style{ - Show: true, - }, - }, - YAxis: chart.YAxis{ - Style: chart.Style{ - Show: true, - }, - }, - Series: []chart.Series{ - chart.TimeSeries{ - Name: "vea", - XValues: vx, - YValues: vy, - Style: chart.Style{ - Show: true, - StrokeColor: chart.GetDefaultSeriesStrokeColor(0), - FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64), - }, - }, - chart.TimeSeries{ - Name: "spy", - XValues: cx, - YValues: cy, - YAxis: chart.YAxisSecondary, // key (!) - Style: chart.Style{ - Show: true, - StrokeColor: chart.GetDefaultSeriesStrokeColor(1), - FillColor: chart.GetDefaultSeriesStrokeColor(1).WithAlpha(64), - }, - }, - chart.AnnotationSeries{ - Name: fmt.Sprintf("%s - Last Value", "vea"), - Style: chart.Style{ - Show: true, - StrokeColor: chart.GetDefaultSeriesStrokeColor(0), - }, - Annotations: []chart.Annotation{ - chart.Annotation{ - X: float64(vx[len(vx)-1].Unix()), - Y: vy[len(vy)-1], - Label: fmt.Sprintf("%s - %s", "vea", chart.FloatValueFormatter(vy[len(vy)-1])), - }, - }, - }, - chart.AnnotationSeries{ - Name: fmt.Sprintf("%s - Last Value", "goog"), - Style: chart.Style{ - Show: true, - StrokeColor: chart.GetDefaultSeriesStrokeColor(1), - }, - YAxis: chart.YAxisSecondary, // key (!) - Annotations: []chart.Annotation{ - chart.Annotation{ - X: float64(cx[len(cx)-1].Unix()), - Y: cy[len(cy)-1], - Label: fmt.Sprintf("%s - %s", "goog", chart.FloatValueFormatter(cy[len(cy)-1])), - }, - }, - }, - }, -} -graph.Render(chart.PNG, buffer) -``` - -# Moving Averages - -You can now also graph a moving average of a series using a special `MovingAverageSeries` that takes an `InnerSeries` as a required argument. - - ![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/ma_goog_ltm.png) - - There is a helper method, `GetLastValue` on the `MovingAverageSeries` to aid in creating a last value annotation for the series. - -# More Intense Technical Analysis - -You can also have series that produce two values, i.e. a series that implements `BoundedValueProvider` and the `GetBoundedValue(int)(x,y1,y2 float64)` method. An example of a `BoundedValueProvider` is the included `BollingerBandsSeries`. +Bollinger Bounds: ![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/spy_ltm_bbs.png) -Like the `MovingAverageSeries` this series takes an `InnerSeries` argument as required, and defaults to 10 samples and a `K` value of 2.0 (or two standard deviations in either direction). +# Code Examples + +Actual chart configurations and examples can be found in the `./examples/` directory. They are web servers, so start them with `go run main.go` then access `http://localhost:8080` to see the output. + +# Usage + +Everything starts with the `chart.Chart` object. The bare minimum to draw a chart would be the following: + +```golang + +import ( + ... + "bytes" + ... + "github.com/wcharczuk/go-chart" //exposes "chart" +) + +graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0}, + }, + }, +} + +buffer := bytes.NewBuffer([]byte{}) +err := graph.Render(chart.PNG, buffer) +``` + +Explanation of the above: A `chart` can have many `Series`, a `Series` is a collection of things that need to be drawn according to the X range and the Y range(s). + +Here, we have a single series with x range values as float64s, rendered to a PNG. Note; we can pass any type of `io.Writer` into `Render(...)`, meaning that we can render the chart to a file or a resonse or anything else that implements `io.Writer`. + +# API Overview + +Everything on the `chart.Chart` object has defaults that can be overriden. Whenever a developer sets a property on the chart object, it is to be assumed that value will be used instead of the default. One complication here +is any object's root `chart.Style` object (i.e named `Style`) and the `Show` property specifically, if any other property is set and the `Show` property is unset, it is assumed to be it's default value of `False`. + +The best way to see the api in action is to look at the examples in the `./examples/` directory. # Design Philosophy diff --git a/annotation_series.go b/annotation_series.go index 770fa81..8a9b891 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -39,7 +39,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran Right: 0, Bottom: 0, } - if as.Style.Show { + if as.Style.IsZero() || as.Style.Show { style := as.Style.WithDefaultsFrom(Style{ Font: defaults.Font, FillColor: DefaultAnnotationFillColor, @@ -63,7 +63,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran // Render draws the series. func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - if as.Style.Show { + if as.Style.IsZero() || as.Style.Show { style := as.Style.WithDefaultsFrom(Style{ Font: defaults.Font, FontColor: DefaultTextColor, diff --git a/chart.go b/chart.go index bf82791..5f7a3d6 100644 --- a/chart.go +++ b/chart.go @@ -133,6 +133,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { var miny, maxy float64 = math.MaxFloat64, 0 var minya, maxya float64 = math.MaxFloat64, 0 + // note: a possible future optimization is to not scan the series values if + // all axis are represented by either custom ticks or custom ranges. for _, s := range c.Series { if s.GetStyle().IsZero() || s.GetStyle().Show { seriesAxis := s.GetYAxis() @@ -176,7 +178,15 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { } } - if !c.XAxis.Range.IsZero() { + if len(c.XAxis.Ticks) > 0 { + tickMin, tickMax := math.MaxFloat64, 0.0 + for _, t := range c.XAxis.Ticks { + tickMin = math.Min(tickMin, t.Value) + tickMax = math.Max(tickMax, t.Value) + } + xrange.Min = tickMin + xrange.Max = tickMax + } else if !c.XAxis.Range.IsZero() { xrange.Min = c.XAxis.Range.Min xrange.Max = c.XAxis.Range.Max } else { @@ -184,7 +194,15 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { xrange.Max = maxx } - if !c.YAxis.Range.IsZero() { + if len(c.YAxis.Ticks) > 0 { + tickMin, tickMax := math.MaxFloat64, 0.0 + for _, t := range c.YAxis.Ticks { + tickMin = math.Min(tickMin, t.Value) + tickMax = math.Max(tickMax, t.Value) + } + yrange.Min = tickMin + yrange.Max = tickMax + } else if !c.YAxis.Range.IsZero() { yrange.Min = c.YAxis.Range.Min yrange.Max = c.YAxis.Range.Max } else { @@ -193,7 +211,15 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds() } - if !c.YAxisSecondary.Range.IsZero() { + if len(c.YAxisSecondary.Ticks) > 0 { + tickMin, tickMax := math.MaxFloat64, 0.0 + for _, t := range c.YAxis.Ticks { + tickMin = math.Min(tickMin, t.Value) + tickMax = math.Max(tickMax, t.Value) + } + yrangeAlt.Min = tickMin + yrangeAlt.Max = tickMax + } else if !c.YAxisSecondary.Range.IsZero() { yrangeAlt.Min = c.YAxisSecondary.Range.Min yrangeAlt.Max = c.YAxisSecondary.Range.Max } else { @@ -348,7 +374,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, } func (c Chart) drawBackground(r Renderer) { - DrawBox(r, c.Box(), c.Canvas.WithDefaultsFrom(c.styleDefaultsBackground())) + DrawBox(r, c.Box(), c.Background.WithDefaultsFrom(c.styleDefaultsBackground())) } func (c Chart) drawCanvas(r Renderer, canvasBox Box) { diff --git a/defaults.go b/defaults.go index cf0ebc3..626a547 100644 --- a/defaults.go +++ b/defaults.go @@ -11,7 +11,7 @@ const ( // DefaultChartHeight is the default chart height. DefaultChartHeight = 400 // DefaultChartWidth is the default chart width. - DefaultChartWidth = 200 + DefaultChartWidth = 1024 // DefaultStrokeWidth is the default chart line/stroke width. DefaultStrokeWidth = 1.0 // DefaultAxisLineWidth is the line width of the axis lines. diff --git a/drawing_helpers.go b/drawing_helpers.go index d109da6..85262d0 100644 --- a/drawing_helpers.go +++ b/drawing_helpers.go @@ -185,7 +185,7 @@ func DrawAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string // DrawBox draws a box with a given style. func DrawBox(r Renderer, b Box, s Style) { r.SetFillColor(s.GetFillColor()) - r.SetStrokeColor(s.GetStrokeColor(DefaultStrokeColor)) + r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth)) r.SetStrokeDashArray(s.GetStrokeDashArray()) @@ -223,15 +223,22 @@ func DrawTextCentered(r Renderer, text string, x, y int, s Style) { } // CreateLegend returns a legend renderable function. -func CreateLegend(c *Chart, style Style) Renderable { - return func(r Renderer, cb Box, defaults Style) { - workingStyle := style.WithDefaultsFrom(defaults.WithDefaultsFrom(Style{ +func CreateLegend(c *Chart, userDefaults ...Style) Renderable { + return func(r Renderer, cb Box, chartDefaults Style) { + legendDefaults := Style{ FillColor: drawing.ColorWhite, FontColor: DefaultTextColor, FontSize: 8.0, StrokeColor: DefaultAxisColor, StrokeWidth: DefaultAxisLineWidth, - })) + } + + var legendStyle Style + if len(userDefaults) > 0 { + legendStyle = userDefaults[0].WithDefaultsFrom(chartDefaults.WithDefaultsFrom(legendDefaults)) + } else { + legendStyle = chartDefaults.WithDefaultsFrom(legendDefaults) + } // DEFAULTS legendPadding := 5 @@ -240,11 +247,11 @@ func CreateLegend(c *Chart, style Style) Renderable { var labels []string var lines []Style - for _, s := range c.Series { + for index, s := range c.Series { if s.GetStyle().IsZero() || s.GetStyle().Show { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) - lines = append(lines, s.GetStyle()) + lines = append(lines, s.GetStyle().WithDefaultsFrom(c.styleDefaultsSeries(index))) } } } @@ -259,8 +266,9 @@ func CreateLegend(c *Chart, style Style) Renderable { Left: legend.Left + legendPadding, } - r.SetFontColor(workingStyle.GetFontColor()) - r.SetFontSize(workingStyle.GetFontSize()) + r.SetFont(legendStyle.GetFont()) + r.SetFontColor(legendStyle.GetFontColor()) + r.SetFontSize(legendStyle.GetFontSize()) // measure for x := 0; x < len(labels); x++ { @@ -273,7 +281,7 @@ func CreateLegend(c *Chart, style Style) Renderable { } legend = legend.Grow(legendContent) - DrawBox(r, legend, workingStyle) + DrawBox(r, legend, legendStyle) legendContent.Right = legend.Right - legendPadding legendContent.Bottom = legend.Bottom - legendPadding @@ -283,6 +291,7 @@ func CreateLegend(c *Chart, style Style) Renderable { for x := 0; x < len(labels); x++ { if len(labels[x]) > 0 { tb := r.MeasureText(labels[x]) + ycursor += tb.Height() r.Text(labels[x], tx, ycursor) diff --git a/examples/annotations/main.go b/examples/annotations/main.go new file mode 100644 index 0000000..74242e4 --- /dev/null +++ b/examples/annotations/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + + /* + In this example we add an `Annotation` series, which is a special type of series that + draws annotation labels at given X and Y values (as translated by their respective ranges). + + It is important to not that the chart automatically sizes the canvas box to fit the annotations, + As well as automatically assign a series color for the `Stroke` or border component of the series. + + The annotation series is most often used by the original author to show the last value of another series, but + they can be used in other capacities as well. + */ + + graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + chart.AnnotationSeries{ + Annotations: []chart.Annotation{ + {X: 1.0, Y: 1.0, Label: "One"}, + {X: 2.0, Y: 2.0, Label: "Two"}, + {X: 3.0, Y: 3.0, Label: "Three"}, + {X: 4.0, Y: 4.0, Label: "Four"}, + {X: 5.0, Y: 5.0, Label: "Five"}, + }, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/axes/main.go b/examples/axes/main.go new file mode 100644 index 0000000..dc8d6be --- /dev/null +++ b/examples/axes/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + + /* + The below will draw the same chart as the `basic` example, except with both the x and y axes turned on. + In this case, both the x and y axis ticks are generated automatically, the x and y ranges are established automatically, the canvas "box" is adjusted to fit the space the axes occupy so as not to clip. + */ + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Style: chart.Style{ + Show: true, //enables / displays the x-axis + }, + }, + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, //enables / displays the y-axis + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..2e86ac5 --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func drawChartWide(res http.ResponseWriter, req *http.Request) { + graph := chart.Chart{ + Width: 1920, //this overrides the default. + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.HandleFunc("/wide", drawChartWide) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/custom_formatters/main.go b/examples/custom_formatters/main.go new file mode 100644 index 0000000..5da4b00 --- /dev/null +++ b/examples/custom_formatters/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + /* + In this example we use a custom `ValueFormatter` for the y axis, letting us specify how to format text of the y-axis ticks. + You can also do this for the x-axis, or the secondary y-axis. + This example also shows what the chart looks like with the x-axis left off or not shown. + */ + + graph := chart.Chart{ + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, + }, + ValueFormatter: func(v interface{}) string { + if vf, isFloat := v.(float64); isFloat { + return fmt.Sprintf("%0.6f", vf) + } + return "" + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/custom_ranges/main.go b/examples/custom_ranges/main.go new file mode 100644 index 0000000..b6a6657 --- /dev/null +++ b/examples/custom_ranges/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + /* + In this example we set a custom range for the y-axis, overriding the automatic range generation. + Note: the chart will still generate the ticks automatically based on the custom range, so the intervals may be a bit weird. + */ + + graph := chart.Chart{ + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, + }, + Range: chart.Range{ + Min: 0.0, + Max: 10.0, + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/custom_styles/main.go b/examples/custom_styles/main.go new file mode 100644 index 0000000..4c93856 --- /dev/null +++ b/examples/custom_styles/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/drawing" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + /* + In this example we set some custom colors for the series and the chart background and canvas. + */ + graph := chart.Chart{ + Background: chart.Style{ + FillColor: drawing.ColorBlue, + }, + Canvas: chart.Style{ + FillColor: drawing.ColorFromHex("efefef"), + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + Style: chart.Style{ + Show: true, //note; if we set ANY other properties, we must set this to true. + StrokeColor: drawing.ColorRed, // will supercede defaults + FillColor: drawing.ColorRed.WithAlpha(64), // will supercede defaults + }, + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/custom_ticks/main.go b/examples/custom_ticks/main.go new file mode 100644 index 0000000..34bb712 --- /dev/null +++ b/examples/custom_ticks/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + /* + In this example we set a custom set of ticks to use for the y-axis. It can be (almost) whatever you want, including some custom labels for ticks. + Custom ticks will supercede a custom range, which will supercede automatic generation based on series values. + */ + + graph := chart.Chart{ + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, + }, + Range: chart.Range{ + Min: 0.0, + Max: 4.0, + }, + Ticks: []chart.Tick{ + {0.0, "0.00"}, + {2.0, "2.00"}, + {4.0, "4.00"}, + {6.0, "6.00"}, + {8.0, "Eight"}, + {10.0, "Ten"}, + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/legend/main.go b/examples/legend/main.go new file mode 100644 index 0000000..b1fcfd2 --- /dev/null +++ b/examples/legend/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + + /* + In this example we add a `Renderable` or a custom component to the `Elements` array. + In this specific case it is a pre-built renderable (`CreateLegend`) that draws a legend for the chart's series. + If you like, you can use `CreateLegend` as a template for writing your own renderable, or even your own legend. + */ + + graph := chart.Chart{ + Series: []chart.Series{ + chart.ContinuousSeries{ + Name: "A test series", + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + //note we have to do this as a separate step because we need a reference to graph + graph.Elements = []chart.Renderable{ + chart.CreateLegend(&graph), + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/simple_moving_average/main.go b/examples/simple_moving_average/main.go new file mode 100644 index 0000000..ff48885 --- /dev/null +++ b/examples/simple_moving_average/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + + /* + In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument. + InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. + */ + + mainSeries := chart.ContinuousSeries{ + Name: "A test series", + XValues: chart.Seq(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. + } + + // note we create a SimpleMovingAverage series by assignin the inner series. + // we need to use a reference because `.Render()` needs to modify state within the series. + smaSeries := &chart.SimpleMovingAverageSeries{ + InnerSeries: mainSeries, + } // we can optionally set the `WindowSize` property which alters how the moving average is calculated. + + graph := chart.Chart{ + Series: []chart.Series{ + mainSeries, + smaSeries, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/examples/twoaxis/main.go b/examples/twoaxis/main.go new file mode 100644 index 0000000..7656e2c --- /dev/null +++ b/examples/twoaxis/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + + /* + In this example we add a second series, and assign it to the secondary y axis, giving that series it's own range. + + We also enable all of the axes by setting the `Show` propery of their respective styles to `true`. + */ + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Style: chart.Style{ + Show: true, //enables / displays the x-axis + }, + }, + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, //enables / displays the y-axis + }, + }, + YAxisSecondary: chart.YAxis{ + Style: chart.Style{ + Show: true, //enables / displays the secondary y-axis + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + chart.ContinuousSeries{ + YAxis: chart.YAxisSecondary, + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{50.0, 40.0, 30.0, 20.0, 10.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/testserver/main.go b/testserver/main.go deleted file mode 100644 index c6be9d1..0000000 --- a/testserver/main.go +++ /dev/null @@ -1,255 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "log" - "math/rand" - "net/http" - "time" - - "github.com/wcharczuk/go-chart" - "github.com/wcharczuk/go-chart/drawing" - "github.com/wcharczuk/go-web" -) - -func chartHandler(rc *web.RequestContext) web.ControllerResult { - rnd := rand.New(rand.NewSource(0)) - format, err := rc.RouteParameter("format") - if err != nil { - format = "png" - } - - if format == "png" { - rc.Response.Header().Set("Content-Type", "image/png") - } else { - rc.Response.Header().Set("Content-Type", "image/svg+xml") - } - - var s1x []time.Time - for x := 0; x < 100; x++ { - s1x = append([]time.Time{time.Now().AddDate(0, 0, -1*x)}, s1x...) - } - var s1y []float64 - for x := 0; x < 100; x++ { - s1y = append(s1y, rnd.Float64()*1024) - } - - s1 := chart.TimeSeries{ - Name: "a", - XValues: s1x, - YValues: s1y, - Style: chart.Style{ - Show: true, - //FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64), - }, - } - - s1lv := chart.AnnotationSeries{ - Name: fmt.Sprintf("Last Value"), - Style: chart.Style{ - Show: true, - StrokeColor: chart.GetDefaultSeriesStrokeColor(0), - }, - Annotations: []chart.Annotation{ - { - X: float64(s1x[len(s1x)-1].Unix()), - Y: s1y[len(s1y)-1], - Label: fmt.Sprintf("%s - %s", "test", chart.FloatValueFormatter(s1y[len(s1y)-1])), - }, - }, - } - - s1bb := &chart.BollingerBandsSeries{ - Name: "BBS", - Style: chart.Style{ - Show: true, - StrokeColor: chart.DefaultAxisColor, - FillColor: chart.DefaultAxisColor.WithAlpha(64), - }, - K: 2.0, - WindowSize: 10, - InnerSeries: s1, - } - - s1sma := &chart.SimpleMovingAverageSeries{ - Style: chart.Style{ - Show: true, - StrokeColor: drawing.ColorRed, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: s1, - } - - s1ema := &chart.ExponentialMovingAverageSeries{ - Style: chart.Style{ - Show: true, - StrokeColor: drawing.ColorBlue, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: s1, - } - - c := chart.Chart{ - Title: "A Test Chart", - TitleStyle: chart.Style{ - Show: false, - }, - Width: 1024, - Height: 400, - XAxis: chart.XAxis{ - Style: chart.Style{ - Show: true, - }, - }, - YAxis: chart.YAxis{ - Style: chart.Style{ - Show: true, - }, - Zero: chart.GridLine{ - Style: chart.Style{ - Show: true, - StrokeWidth: 1.0, - }, - }, - GridMajorStyle: chart.Style{ - Show: false, - }, - GridMinorStyle: chart.Style{ - Show: true, - }, - }, - Series: []chart.Series{ - s1bb, - s1, - s1lv, - s1sma, - s1ema, - }, - } - - if format == "png" { - err = c.Render(chart.PNG, rc.Response) - } else { - err = c.Render(chart.SVG, rc.Response) - } - if err != nil { - return rc.API().InternalError(err) - } - rc.Response.WriteHeader(http.StatusOK) - return nil -} - -func boxHandler(rc *web.RequestContext) web.ControllerResult { - r, err := chart.PNG(1024, 1024) - if err != nil { - rc.API().InternalError(err) - } - - f, err := chart.GetDefaultFont() - if err != nil { - return rc.API().InternalError(err) - } - - //1:1 128wx128h @ 64,64 - a := chart.Box{Top: 64, Left: 64, Right: 192, Bottom: 192} - - // 3:2 256x170 @ 16, 16 - //b := chart.Box{Top: 16, Left: 16, Right: 256, Bottom: 170} - - // 2:3 170x256 @ 16, 16 - c := chart.Box{Top: 16, Left: 16, Right: 170, Bottom: 256} - - //fitb := a.Fit(b) - fitc := a.Fit(c) - //growb := a.Grow(b) - //growc := a.Grow(c) - //grow := a.Grow(b).Grow(c) - - conc := a.Constrain(c) - - boxStyle := chart.Style{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1.0, - Font: f, - FontSize: 18.0, - } - - computedBoxStyle := chart.Style{ - StrokeColor: drawing.ColorRed, - StrokeWidth: 1.0, - Font: f, - FontSize: 18.0, - } - - chart.DrawBox(r, a, boxStyle) - //chart.DrawBox(r, b, boxStyle) - chart.DrawBox(r, c, boxStyle) - //chart.DrawBox(r, fitb, computedBoxStyle) - chart.DrawBox(r, fitc, computedBoxStyle) - /*chart.DrawBox(r, growb, computedBoxStyle) - chart.DrawBox(r, growc, computedBoxStyle) - chart.DrawBox(r, grow, computedBoxStyle)*/ - chart.DrawBox(r, conc, computedBoxStyle) - - ax, ay := a.Center() - chart.DrawTextCentered(r, "a", ax, ay, boxStyle.WithDefaultsFrom(chart.Style{ - FillColor: boxStyle.StrokeColor, - })) - - /*bx, by := b.Center() - chart.DrawTextCentered(r, "b", bx, by, boxStyle.WithDefaultsFrom(chart.Style{ - FillColor: boxStyle.StrokeColor, - }))*/ - - cx, cy := c.Center() - chart.DrawTextCentered(r, "c", cx, cy, boxStyle.WithDefaultsFrom(chart.Style{ - FillColor: boxStyle.StrokeColor, - })) - - /*fbx, fby := fitb.Center() - chart.DrawTextCentered(r, "a fit b", fbx, fby, computedBoxStyle.WithDefaultsFrom(chart.Style{ - FillColor: computedBoxStyle.StrokeColor, - }))*/ - - fcx, fcy := fitc.Center() - chart.DrawTextCentered(r, "a fit c", fcx, fcy, computedBoxStyle.WithDefaultsFrom(chart.Style{ - FillColor: computedBoxStyle.StrokeColor, - })) - - /*gbx, gby := growb.Center() - chart.DrawTextCentered(r, "a grow b", gbx, gby, computedBoxStyle.WithDefaultsFrom(chart.Style{ - FillColor: computedBoxStyle.StrokeColor, - })) - - gcx, gcy := growc.Center() - chart.DrawTextCentered(r, "a grow c", gcx, gcy, computedBoxStyle.WithDefaultsFrom(chart.Style{ - FillColor: computedBoxStyle.StrokeColor, - }))*/ - - ccx, ccy := conc.Center() - chart.DrawTextCentered(r, "a const c", ccx, ccy, computedBoxStyle.WithDefaultsFrom(chart.Style{ - FillColor: computedBoxStyle.StrokeColor, - })) - - rc.Response.Header().Set("Content-Type", "image/png") - buffer := bytes.NewBuffer([]byte{}) - err = r.Save(buffer) - if err != nil { - return rc.API().InternalError(err) - } - return rc.Raw(buffer.Bytes()) -} - -func main() { - app := web.New() - app.SetName("Chart Test Server") - app.SetLogger(web.NewStandardOutputLogger()) - app.GET("/", chartHandler) - app.GET("/format/:format", chartHandler) - app.GET("/favico.ico", func(rc *web.RequestContext) web.ControllerResult { - return rc.Raw([]byte{}) - }) - app.GET("/box", boxHandler) - log.Fatal(app.Start()) -}