works more or less
This commit is contained in:
parent
17b28beae8
commit
9c65a94050
10 changed files with 248 additions and 43 deletions
39
_examples/scatter/main.go
Normal file
39
_examples/scatter/main.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/wcharczuk/go-chart"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||
println("drawing scatter plot")
|
||||
graph := chart.Chart{
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
Style: chart.Style{
|
||||
Show: true,
|
||||
StrokeWidth: chart.Disabled,
|
||||
DotWidth: 3,
|
||||
DotColor: drawing.ColorRed,
|
||||
},
|
||||
XValues: chart.Sequence.Random(32, 1024),
|
||||
YValues: chart.Sequence.Random(32, 1024),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
err := graph.Render(chart.PNG, res)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", drawChart)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
4
chart.go
4
chart.go
|
@ -141,8 +141,10 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
|||
|
||||
func (c Chart) checkHasVisibleSeries() error {
|
||||
hasVisibleSeries := false
|
||||
var style Style
|
||||
for _, s := range c.Series {
|
||||
hasVisibleSeries = hasVisibleSeries || (s.GetStyle().IsZero() || s.GetStyle().Show)
|
||||
style = s.GetStyle()
|
||||
hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show)
|
||||
}
|
||||
if !hasVisibleSeries {
|
||||
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
|
||||
|
|
|
@ -14,6 +14,8 @@ const (
|
|||
DefaultChartWidth = 1024
|
||||
// DefaultStrokeWidth is the default chart stroke width.
|
||||
DefaultStrokeWidth = 0.0
|
||||
// DefaultDotWidth is the default chart dot width.
|
||||
DefaultDotWidth = 0.0
|
||||
// DefaultSeriesLineWidth is the default line width.
|
||||
DefaultSeriesLineWidth = 1.0
|
||||
// DefaultAxisLineWidth is the line width of the axis lines.
|
||||
|
|
37
draw.go
37
draw.go
|
@ -27,9 +27,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
var vx, vy float64
|
||||
var x, y int
|
||||
|
||||
fill := style.GetFillColor()
|
||||
if !fill.IsZero() {
|
||||
style.GetFillOptions().WriteToRenderer(r)
|
||||
if style.ShouldDrawStroke() && style.ShouldDrawFill() {
|
||||
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
|
@ -43,16 +42,32 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
|||
r.Fill()
|
||||
}
|
||||
|
||||
style.GetStrokeOptions().WriteToRenderer(r)
|
||||
if style.ShouldDrawStroke() {
|
||||
style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
||||
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
r.LineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
if style.ShouldDrawDot() {
|
||||
dotWidth := style.GetDotWidth()
|
||||
|
||||
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||
|
||||
for i := 0; i < vs.Len(); i++ {
|
||||
vx, vy = vs.GetValue(i)
|
||||
x = cl + xrange.Translate(vx)
|
||||
y = cb - yrange.Translate(vy)
|
||||
|
||||
r.Circle(dotWidth, x, y)
|
||||
r.FillStroke()
|
||||
}
|
||||
}
|
||||
r.Stroke()
|
||||
}
|
||||
|
||||
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
import "math"
|
||||
|
||||
const (
|
||||
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
|
||||
|
@ -98,31 +96,60 @@ func SubdivideQuad(c, c1, c2 []float64) {
|
|||
return
|
||||
}
|
||||
|
||||
func traceWindowIndices(i int) (startAt, endAt int) {
|
||||
startAt = i * 6
|
||||
endAt = startAt + 6
|
||||
return
|
||||
}
|
||||
|
||||
func traceCalcDeltas(c []float64) (dx, dy, d float64) {
|
||||
dx = c[4] - c[0]
|
||||
dy = c[5] - c[1]
|
||||
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||
return
|
||||
}
|
||||
|
||||
func traceIsFlat(dx, dy, d, threshold float64) bool {
|
||||
return (d * d) < threshold*(dx*dx+dy*dy)
|
||||
}
|
||||
|
||||
func traceGetWindow(curves []float64, i int) []float64 {
|
||||
startAt, endAt := traceWindowIndices(i)
|
||||
return curves[startAt:endAt]
|
||||
}
|
||||
|
||||
// TraceQuad generate lines subdividing the curve using a Liner
|
||||
// flattening_threshold helps determines the flattening expectation of the curve
|
||||
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||
const curveLen = CurveRecursionLimit * 6
|
||||
const curveEndIndex = curveLen - 1
|
||||
const lastIteration = CurveRecursionLimit - 1
|
||||
|
||||
// Allocates curves stack
|
||||
var curves [CurveRecursionLimit * 6]float64
|
||||
curves := make([]float64, curveLen)
|
||||
|
||||
// copy 6 elements from the quad path to the stack
|
||||
copy(curves[0:6], quad[0:6])
|
||||
i := 0
|
||||
// current curve
|
||||
|
||||
var i int
|
||||
var c []float64
|
||||
var dx, dy, d float64
|
||||
|
||||
for i >= 0 {
|
||||
c = curves[i*6:]
|
||||
dx = c[4] - c[0]
|
||||
dy = c[5] - c[1]
|
||||
c = traceGetWindow(curves, i)
|
||||
dx, dy, d = traceCalcDeltas(c)
|
||||
|
||||
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||
// bail early if the distance is 0
|
||||
if d == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// if it's flat then trace a line
|
||||
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
||||
if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
|
||||
t.LineTo(c[4], c[5])
|
||||
i--
|
||||
} else {
|
||||
// second half of bezier go lower onto the stack
|
||||
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
|
||||
SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
|
35
drawing/curve_test.go
Normal file
35
drawing/curve_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package drawing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assert "github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
type point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
type mockLine struct {
|
||||
inner []point
|
||||
}
|
||||
|
||||
func (ml *mockLine) LineTo(x, y float64) {
|
||||
ml.inner = append(ml.inner, point{x, y})
|
||||
}
|
||||
|
||||
func (ml mockLine) Len() int {
|
||||
return len(ml.inner)
|
||||
}
|
||||
|
||||
func TestTraceQuad(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Quad
|
||||
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||
// do the 9->12 circle segment
|
||||
quad := []float64{10, 20, 20, 20, 20, 10}
|
||||
liner := &mockLine{}
|
||||
TraceQuad(liner, quad, 0.5)
|
||||
assert.NotZero(liner.Len())
|
||||
}
|
|
@ -23,10 +23,10 @@ type Flattener interface {
|
|||
// Flatten convert curves into straight segments keeping join segments info
|
||||
func Flatten(path *Path, flattener Flattener, scale float64) {
|
||||
// First Point
|
||||
var startX, startY float64 = 0, 0
|
||||
var startX, startY float64
|
||||
// Current Point
|
||||
var x, y float64 = 0, 0
|
||||
i := 0
|
||||
var x, y float64
|
||||
var i int
|
||||
for _, cmp := range path.Components {
|
||||
switch cmp {
|
||||
case MoveToComponent:
|
||||
|
@ -43,6 +43,7 @@ func Flatten(path *Path, flattener Flattener, scale float64) {
|
|||
flattener.LineJoin()
|
||||
i += 2
|
||||
case QuadCurveToComponent:
|
||||
// we include the previous point for the start of the curve
|
||||
TraceQuad(flattener, path.Points[i-2:], 0.5)
|
||||
x, y = path.Points[i+2], path.Points[i+3]
|
||||
flattener.LineTo(x, y)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
|
@ -116,17 +117,17 @@ func (rr *rasterRenderer) FillStroke() {
|
|||
rr.gc.FillStroke()
|
||||
}
|
||||
|
||||
// Circle implements the interface method.
|
||||
// Circle fully draws and strokes a circle at a given point.
|
||||
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
|
||||
fmt.Printf("RasterRenderer.Circle(%f, %d, %d)\n", radius, x, y)
|
||||
xf := float64(x)
|
||||
yf := float64(y)
|
||||
rr.gc.MoveTo(xf-radius, yf) //9
|
||||
rr.gc.QuadCurveTo(xf, yf, xf, yf-radius) //12
|
||||
rr.gc.QuadCurveTo(xf, yf, xf+radius, yf) //3
|
||||
rr.gc.QuadCurveTo(xf, yf, xf, yf+radius) //6
|
||||
rr.gc.QuadCurveTo(xf, yf, xf-radius, yf) //9
|
||||
rr.gc.Close()
|
||||
rr.gc.FillStroke()
|
||||
|
||||
rr.gc.MoveTo(xf-radius, yf) //9
|
||||
rr.gc.QuadCurveTo(xf-radius, yf-radius, xf, yf-radius) //12
|
||||
rr.gc.QuadCurveTo(xf+radius, yf-radius, xf+radius, yf) //3
|
||||
rr.gc.QuadCurveTo(xf+radius, yf+radius, xf, yf+radius) //6
|
||||
rr.gc.QuadCurveTo(xf-radius, yf+radius, xf-radius, yf) //9
|
||||
}
|
||||
|
||||
// SetFont implements the interface method.
|
||||
|
|
14
sequence.go
14
sequence.go
|
@ -8,10 +8,14 @@ import (
|
|||
var (
|
||||
// Sequence contains some sequence utilities.
|
||||
// These utilities can be useful for generating test data.
|
||||
Sequence = &sequence{}
|
||||
Sequence = &sequence{
|
||||
rnd: rand.New(rand.NewSource(time.Now().Unix())),
|
||||
}
|
||||
)
|
||||
|
||||
type sequence struct{}
|
||||
type sequence struct {
|
||||
rnd *rand.Rand
|
||||
}
|
||||
|
||||
// Float64 produces an array of floats from [start,end] by optional steps.
|
||||
func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
||||
|
@ -35,11 +39,10 @@ func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
|||
|
||||
// Random generates a fixed length sequence of random values between (0, scale).
|
||||
func (s sequence) Random(samples int, scale float64) []float64 {
|
||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
values := make([]float64, samples)
|
||||
|
||||
for x := 0; x < samples; x++ {
|
||||
values[x] = rnd.Float64() * scale
|
||||
values[x] = s.rnd.Float64() * scale
|
||||
}
|
||||
|
||||
return values
|
||||
|
@ -47,11 +50,10 @@ func (s sequence) Random(samples int, scale float64) []float64 {
|
|||
|
||||
// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale)
|
||||
func (s sequence) RandomWithAverage(samples int, average, scale float64) []float64 {
|
||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||
values := make([]float64, samples)
|
||||
|
||||
for x := 0; x < samples; x++ {
|
||||
jitter := scale - (rnd.Float64() * (2 * scale))
|
||||
jitter := scale - (s.rnd.Float64() * (2 * scale))
|
||||
values[x] = average + jitter
|
||||
}
|
||||
|
||||
|
|
83
style.go
83
style.go
|
@ -8,6 +8,12 @@ import (
|
|||
"github.com/wcharczuk/go-chart/drawing"
|
||||
)
|
||||
|
||||
const (
|
||||
// Disabled indicates if the value should be interpreted as set intentionally to zero.
|
||||
// this is because golang optionals aren't here yet.
|
||||
Disabled = -1
|
||||
)
|
||||
|
||||
// StyleShow is a prebuilt style with the `Show` property set to true.
|
||||
func StyleShow() Style {
|
||||
return Style{
|
||||
|
@ -24,7 +30,11 @@ type Style struct {
|
|||
StrokeColor drawing.Color
|
||||
StrokeDashArray []float64
|
||||
|
||||
DotColor drawing.Color
|
||||
DotWidth float64
|
||||
|
||||
FillColor drawing.Color
|
||||
|
||||
FontSize float64
|
||||
FontColor drawing.Color
|
||||
Font *truetype.Font
|
||||
|
@ -38,7 +48,14 @@ type Style struct {
|
|||
|
||||
// IsZero returns if the object is set or not.
|
||||
func (s Style) IsZero() bool {
|
||||
return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontColor.IsZero() && s.FontSize == 0 && s.Font == nil
|
||||
return s.StrokeColor.IsZero() &&
|
||||
s.StrokeWidth == 0 &&
|
||||
s.DotColor.IsZero() &&
|
||||
s.DotWidth == 0 &&
|
||||
s.FillColor.IsZero() &&
|
||||
s.FontColor.IsZero() &&
|
||||
s.FontSize == 0 &&
|
||||
s.Font == nil
|
||||
}
|
||||
|
||||
// String returns a text representation of the style.
|
||||
|
@ -83,6 +100,18 @@ func (s Style) String() string {
|
|||
output = append(output, "\"stroke_dash_array\": null")
|
||||
}
|
||||
|
||||
if s.DotWidth >= 0 {
|
||||
output = append(output, fmt.Sprintf("\"dot_width\": %0.2f", s.DotWidth))
|
||||
} else {
|
||||
output = append(output, "\"dot_width\": null")
|
||||
}
|
||||
|
||||
if !s.DotColor.IsZero() {
|
||||
output = append(output, fmt.Sprintf("\"dot_color\": %s", s.DotColor.String()))
|
||||
} else {
|
||||
output = append(output, "\"dot_color\": null")
|
||||
}
|
||||
|
||||
if !s.FillColor.IsZero() {
|
||||
output = append(output, fmt.Sprintf("\"fill_color\": %s", s.FillColor.String()))
|
||||
} else {
|
||||
|
@ -132,6 +161,17 @@ func (s Style) GetFillColor(defaults ...drawing.Color) drawing.Color {
|
|||
return s.FillColor
|
||||
}
|
||||
|
||||
// GetDotColor returns the stroke color.
|
||||
func (s Style) GetDotColor(defaults ...drawing.Color) drawing.Color {
|
||||
if s.DotColor.IsZero() {
|
||||
if len(defaults) > 0 {
|
||||
return defaults[0]
|
||||
}
|
||||
return drawing.ColorTransparent
|
||||
}
|
||||
return s.DotColor
|
||||
}
|
||||
|
||||
// GetStrokeWidth returns the stroke width.
|
||||
func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
||||
if s.StrokeWidth == 0 {
|
||||
|
@ -143,6 +183,17 @@ func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
|||
return s.StrokeWidth
|
||||
}
|
||||
|
||||
// GetDotWidth returns the dot width for scatter plots.
|
||||
func (s Style) GetDotWidth(defaults ...float64) float64 {
|
||||
if s.DotWidth == 0 {
|
||||
if len(defaults) > 0 {
|
||||
return defaults[0]
|
||||
}
|
||||
return DefaultDotWidth
|
||||
}
|
||||
return s.DotWidth
|
||||
}
|
||||
|
||||
// GetStrokeDashArray returns the stroke dash array.
|
||||
func (s Style) GetStrokeDashArray(defaults ...[]float64) []float64 {
|
||||
if len(s.StrokeDashArray) == 0 {
|
||||
|
@ -288,6 +339,10 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
|||
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
|
||||
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
|
||||
final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
|
||||
|
||||
final.DotColor = s.GetDotColor(defaults.DotColor)
|
||||
final.DotWidth = s.GetDotWidth(defaults.DotWidth)
|
||||
|
||||
final.FillColor = s.GetFillColor(defaults.FillColor)
|
||||
final.FontColor = s.GetFontColor(defaults.FontColor)
|
||||
final.FontSize = s.GetFontSize(defaults.FontSize)
|
||||
|
@ -298,6 +353,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
|||
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
||||
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
||||
final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -317,6 +373,16 @@ func (s Style) GetFillOptions() Style {
|
|||
}
|
||||
}
|
||||
|
||||
// GetDotOptions returns the dot components.
|
||||
func (s Style) GetDotOptions() Style {
|
||||
return Style{
|
||||
StrokeDashArray: nil,
|
||||
FillColor: s.DotColor,
|
||||
StrokeColor: s.DotColor,
|
||||
StrokeWidth: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFillAndStrokeOptions returns the fill and stroke components.
|
||||
func (s Style) GetFillAndStrokeOptions() Style {
|
||||
return Style{
|
||||
|
@ -340,3 +406,18 @@ func (s Style) GetTextOptions() Style {
|
|||
TextRotationDegrees: s.TextRotationDegrees,
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldDrawStroke tells drawing functions if they should draw the stroke.
|
||||
func (s Style) ShouldDrawStroke() bool {
|
||||
return !s.StrokeColor.IsZero() && s.StrokeWidth > 0
|
||||
}
|
||||
|
||||
// ShouldDrawDot tells drawing functions if they should draw the dot.
|
||||
func (s Style) ShouldDrawDot() bool {
|
||||
return !s.DotColor.IsZero() && s.DotWidth > 0
|
||||
}
|
||||
|
||||
// ShouldDrawFill tells drawing functions if they should draw the stroke.
|
||||
func (s Style) ShouldDrawFill() bool {
|
||||
return !s.FillColor.IsZero()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue