diff --git a/box.go b/box.go new file mode 100644 index 0000000..6ed8ed1 --- /dev/null +++ b/box.go @@ -0,0 +1,65 @@ +package chart + +import "fmt" + +// Box represents the main 4 dimensions of a box. +type Box struct { + Top int + Left int + Right int + Bottom int +} + +// IsZero returns if the box is set or not. +func (b Box) IsZero() bool { + return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0 +} + +// String returns a string representation of the box. +func (b Box) String() string { + return fmt.Sprintf("Box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom) +} + +// GetTop returns a coalesced value with a default. +func (b Box) GetTop(defaults ...int) int { + if b.Top == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return 0 + } + return b.Top +} + +// GetLeft returns a coalesced value with a default. +func (b Box) GetLeft(defaults ...int) int { + if b.Left == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return 0 + } + return b.Left +} + +// GetRight returns a coalesced value with a default. +func (b Box) GetRight(defaults ...int) int { + if b.Right == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return 0 + } + return b.Right +} + +// GetBottom returns a coalesced value with a default. +func (b Box) GetBottom(defaults ...int) int { + if b.Bottom == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return 0 + } + return b.Bottom +} diff --git a/chart.go b/chart.go index 0b282c3..f2d72bf 100644 --- a/chart.go +++ b/chart.go @@ -1,138 +1,83 @@ package chart import ( - "image/color" "io" "github.com/golang/freetype/truetype" ) // Chart is what we're drawing. -/* - The chart box model is as follows: - 0,0 width,0 - cl,ct cr,ct - cl,cb cr,cb - 0, height width,height -*/ type Chart struct { - Title string - TitleFontSize float64 + Title string + TitleStyle Style Width int Height int - Padding int + Padding Box - BackgroundColor color.RGBA - CanvasBackgroundColor color.RGBA + Background Style + Canvas Style + Axes Style + FinalValueLabel Style - AxisShow bool - AxisStyle Style - AxisFontSize float64 - - CanvasBorderShow bool - CanvasBorderStyle Style - - FinalValueLabelShow bool - FinalValueStyle Style - - FontColor color.RGBA - Font *truetype.Font + XRange Range + YRange Range + Font *truetype.Font Series []Series } -// GetTitleFontSize calculates or returns the title font size. -func (c Chart) GetTitleFontSize() float64 { - if c.TitleFontSize != 0 { - if c.TitleFontSize > DefaultMinimumFontSize { - return c.TitleFontSize - } - } - fontSize := float64(c.Height >> 3) - if fontSize > DefaultMinimumFontSize { - return fontSize - } - return DefaultMinimumFontSize -} - // GetCanvasTop gets the top corner pixel. func (c Chart) GetCanvasTop() int { - return c.Padding + return c.Padding.GetTop(DefaultCanvasPadding.Top) } // GetCanvasLeft gets the left corner pixel. func (c Chart) GetCanvasLeft() int { - return c.Padding + return c.Padding.GetLeft(DefaultCanvasPadding.Left) } // GetCanvasBottom gets the bottom corner pixel. func (c Chart) GetCanvasBottom() int { - return c.Height - c.Padding + return c.Height - c.Padding.GetBottom(DefaultCanvasPadding.Bottom) } // GetCanvasRight gets the right corner pixel. func (c Chart) GetCanvasRight() int { - return c.Width - c.Padding + return c.Width - c.Padding.GetRight(DefaultCanvasPadding.Right) } // GetCanvasWidth returns the width of the canvas. func (c Chart) GetCanvasWidth() int { - if c.Padding > 0 { - return c.Width - (c.Padding << 1) - } - return c.Width + return c.Width - (c.Padding.GetLeft(DefaultCanvasPadding.Left) + c.Padding.GetRight(DefaultCanvasPadding.Right)) } // GetCanvasHeight returns the height of the canvas. func (c Chart) GetCanvasHeight() int { - if c.Padding > 0 { - return c.Height - (c.Padding << 1) - } - return c.Height + return c.Height - (c.Padding.GetTop(DefaultCanvasPadding.Top) + c.Padding.GetBottom(DefaultCanvasPadding.Bottom)) } -// GetBackgroundColor returns the chart background color. -func (c Chart) GetBackgroundColor() color.RGBA { - if ColorIsZero(c.BackgroundColor) { - c.BackgroundColor = DefaultBackgroundColor - } - return c.BackgroundColor -} - -// GetCanvasBackgroundColor returns the canvas background color. -func (c Chart) GetCanvasBackgroundColor() color.RGBA { - if ColorIsZero(c.CanvasBackgroundColor) { - c.CanvasBackgroundColor = DefaultCanvasColor - } - return c.CanvasBackgroundColor -} - -// GetTextFont returns the text font. -func (c Chart) GetTextFont() (*truetype.Font, error) { +// GetFont returns the text font. +func (c Chart) GetFont() (*truetype.Font, error) { if c.Font != nil { return c.Font, nil } return GetDefaultFont() } -// GetTextFontColor returns the text font color. -func (c Chart) GetTextFontColor() color.RGBA { - if ColorIsZero(c.FontColor) { - c.FontColor = DefaultTextColor - } - return c.FontColor -} - // Render renders the chart with the given renderer to the given io.Writer. -func (c Chart) Render(provider RendererProvider, w io.Writer) error { +func (c *Chart) Render(provider RendererProvider, w io.Writer) error { + xrange, yrange := c.initRanges() + println("xrange", xrange.String()) + println("yrange", yrange.String()) + r := provider(c.Width, c.Height) c.drawBackground(r) c.drawCanvas(r) c.drawAxes(r) for _, series := range c.Series { - c.drawSeries(r, series) + c.drawSeries(r, series, xrange, yrange) } err := c.drawTitle(r) if err != nil { @@ -141,46 +86,83 @@ func (c Chart) Render(provider RendererProvider, w io.Writer) error { return r.Save(w) } +func (c Chart) initRanges() (xrange Range, yrange Range) { + //iterate over each series, pull out the min/max for x,y + var didSetFirstValues bool + var globalMinY, globalMinX float64 + var globalMaxY, globalMaxX float64 + for _, s := range c.Series { + seriesLength := s.Len() + for index := 0; index < seriesLength; index++ { + vx, vy := s.GetValue(index) + if didSetFirstValues { + if globalMinX > vx { + globalMinX = vx + } + if globalMinY > vy { + globalMinY = vy + } + if globalMaxX < vx { + globalMaxX = vx + } + if globalMaxY < vy { + globalMaxY = vy + } + } else { + globalMinX, globalMaxX = vx, vx + globalMinY, globalMaxY = vy, vy + didSetFirstValues = true + } + } + } + + if c.XRange.IsZero() { + xrange.Min = globalMinX + xrange.Max = globalMaxX + } else { + xrange.Min = c.XRange.Min + xrange.Max = c.XRange.Max + } + xrange.Domain = c.GetCanvasWidth() + + if c.YRange.IsZero() { + yrange.Min = globalMinY + yrange.Max = globalMaxY + } else { + yrange.Min = c.YRange.Min + yrange.Max = c.YRange.Max + } + yrange.Domain = c.GetCanvasHeight() + + return +} + func (c Chart) drawBackground(r Renderer) { - r.SetStrokeColor(c.GetBackgroundColor()) - r.SetFillColor(c.GetBackgroundColor()) - r.SetLineWidth(0) + r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor)) r.MoveTo(0, 0) r.LineTo(c.Width, 0) r.LineTo(c.Width, c.Height) r.LineTo(0, c.Height) r.LineTo(0, 0) - r.FillStroke() r.Close() + r.Fill() } func (c Chart) drawCanvas(r Renderer) { - if !c.CanvasBorderStyle.IsZero() { - r.SetStrokeColor(c.CanvasBorderStyle.GetStrokeColor()) - r.SetLineWidth(c.CanvasBorderStyle.GetStrokeWidth()) - } else { - r.SetStrokeColor(c.GetCanvasBackgroundColor()) - r.SetLineWidth(0) - } - r.SetFillColor(c.GetCanvasBackgroundColor()) + r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor)) r.MoveTo(c.GetCanvasLeft(), c.GetCanvasTop()) r.LineTo(c.GetCanvasRight(), c.GetCanvasTop()) r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasLeft(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasLeft(), c.GetCanvasTop()) - r.FillStroke() + r.Fill() r.Close() } func (c Chart) drawAxes(r Renderer) { - if c.AxisShow { - if !c.AxisStyle.IsZero() { - r.SetStrokeColor(c.AxisStyle.GetStrokeColor()) - r.SetLineWidth(c.AxisStyle.GetStrokeWidth()) - } else { - r.SetStrokeColor(DefaultAxisColor) - r.SetLineWidth(DefaultAxisLineWidth) - } + if c.Axes.Show { + r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor)) + r.SetLineWidth(c.Axes.GetStrokeWidth(DefaultLineWidth)) r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasRight(), c.GetCanvasTop()) @@ -188,46 +170,48 @@ func (c Chart) drawAxes(r Renderer) { } } -func (c Chart) drawSeries(r Renderer, s Series) { - r.SetLineWidth(s.GetStyle().GetStrokeWidth()) - r.SetStrokeColor(s.GetStyle().GetStrokeColor()) - - xrange := s.GetXRange(c.GetCanvasWidth()) - yrange := s.GetYRange(c.GetCanvasHeight()) +func (c Chart) drawSeries(r Renderer, s Series, xrange, yrange Range) { + r.SetStrokeColor(s.GetStyle().GetStrokeColor(DefaultLineColor)) + r.SetLineWidth(s.GetStyle().GetStrokeWidth(DefaultLineWidth)) if s.Len() == 0 { return } - v0x, v0y := s.GetValue(0) - x0 := xrange.Translate(v0x) - y0 := yrange.Translate(v0y) - r.MoveTo(x0, y0) + px := c.Padding.GetLeft(DefaultCanvasPadding.Left) + py := c.Padding.GetTop(DefaultCanvasPadding.Top) - var vx interface{} - var vy float64 + cw := c.GetCanvasWidth() + + v0x, v0y := s.GetValue(0) + x0 := cw - xrange.Translate(v0x) + y0 := yrange.Translate(v0y) + r.MoveTo(x0+px, y0+py) + + var vx, vy float64 var x, y int - for index := 0; index < s.Len(); index++ { + for index := 1; index < s.Len(); index++ { vx, vy = s.GetValue(index) - x = xrange.Translate(vx) + x = cw - xrange.Translate(vx) y = yrange.Translate(vy) - r.LineTo(x, y) + r.LineTo(x+px, y+py) } r.Stroke() } func (c Chart) drawTitle(r Renderer) error { - if len(c.Title) > 0 { - font, err := c.GetTextFont() + if len(c.Title) > 0 && c.TitleStyle.Show { + font, err := c.GetFont() if err != nil { return err } - r.SetFontColor(c.GetTextFontColor()) + r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor)) r.SetFont(font) - r.SetFontSize(c.GetTitleFontSize()) + titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize) + r.SetFontSize(titleFontSize) textWidth := r.MeasureText(c.Title) titleX := (c.Width >> 1) - (textWidth >> 1) - titleY := c.GetCanvasTop() + int(c.GetTitleFontSize()/2.0) + titleY := c.GetCanvasTop() + int(titleFontSize) r.Text(c.Title, titleX, titleY) } return nil diff --git a/chart_test.go b/chart_test.go index 9ce38e4..9a63b4a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -15,9 +15,13 @@ func TestChartSingleSeries(t *testing.T) { Title: "Hello!", Width: 1024, Height: 400, + YRange: Range{ + Min: 0.0, + Max: 4.0, + }, Series: []Series{ TimeSeries{ - Name: "Goog", + Name: "goog", XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)}, YValues: []float64{1.0, 2.0, 3.0}, }, diff --git a/defaults.go b/defaults.go index fa03d56..7b5f7f0 100644 --- a/defaults.go +++ b/defaults.go @@ -12,9 +12,6 @@ const ( DefaultChartHeight = 400 // DefaultChartWidth is the default chart width. DefaultChartWidth = 200 - // DefaultPadding is the default gap between the image border and - // chart content (referred to as the "canvas"). - DefaultPadding = 10 // DefaultLineWidth is the default chart line width. DefaultLineWidth = 2.0 // DefaultAxisLineWidth is the line width of the axis lines. @@ -23,12 +20,18 @@ const ( DefaultDPI = 120.0 // DefaultMinimumFontSize is the default minimum font size. DefaultMinimumFontSize = 8.0 + // DefaultFontSize is the default font size. + DefaultFontSize = 10.0 + // DefaultTitleFontSize is the default title font size. + DefaultTitleFontSize = 18.0 + // DefaultDateFormat is the default date format. + DefaultDateFormat = "2006-01-02" ) var ( // DefaultBackgroundColor is the default chart background color. // It is equivalent to css color:white. - DefaultBackgroundColor = color.RGBA{R: 255, G: 255, B: 255, A: 255} + DefaultBackgroundColor = color.RGBA{R: 239, G: 239, B: 239, A: 255} //color.RGBA{R: 255, G: 255, B: 255, A: 255} // DefaultCanvasColor is the default chart canvas color. // It is equivalent to css color:white. DefaultCanvasColor = color.RGBA{R: 255, G: 255, B: 255, A: 255} @@ -43,12 +46,17 @@ var ( DefaultBorderColor = color.RGBA{R: 239, G: 239, B: 239, A: 255} // DefaultLineColor is the default (1st) series line color. // It is equivalent to #0074d9. - DefaultLineColor = color.RGBA{R: 0, G: 217, B: 116, A: 255} + DefaultLineColor = color.RGBA{R: 0, G: 116, B: 217, A: 255} // DefaultFillColor is the default fill color. // It is equivalent to #0074d9. DefaultFillColor = color.RGBA{R: 0, G: 217, B: 116, A: 255} ) +var ( + // DefaultCanvasPadding is the default canvas padding config. + DefaultCanvasPadding = Box{Top: 5, Left: 5, Right: 15, Bottom: 15} +) + var ( _defaultFontLock sync.Mutex _defaultFont *truetype.Font diff --git a/range.go b/range.go index 1c342a2..65c5243 100644 --- a/range.go +++ b/range.go @@ -1,97 +1,36 @@ package chart import ( + "fmt" "math" - "time" ) -// Range is a type that translates values from a range to a domain. -type Range interface { - GetMin() interface{} - GetMax() interface{} - Translate(value interface{}) int +// Range represents a continuous range, +type Range struct { + Min float64 + Max float64 + Domain int } -// NewRangeOfFloat64 returns a new Range -func NewRangeOfFloat64(domain int, values ...float64) Range { - min, max := MinAndMax(values...) - return &RangeOfFloat64{ - MinValue: min, - MaxValue: max, - MinMaxDelta: max - min, - Domain: domain, - } +// IsZero returns if the range has been set or not. +func (r Range) IsZero() bool { + return r.Min == 0 && r.Max == 0 && r.Domain == 0 } -// RangeOfFloat64 represents a continuous range -// of float64 values mapped to a [0...WindowMaxValue] -// interval. -type RangeOfFloat64 struct { - MinValue float64 - MaxValue float64 - MinMaxDelta float64 - Domain int +// Delta returns the difference between the min and max value. +func (r Range) Delta() float64 { + return r.Max - r.Min } -// GetMin implements the interface method. -func (r RangeOfFloat64) GetMin() interface{} { - return r.MinValue -} - -// GetMax implements the interface method. -func (r RangeOfFloat64) GetMax() interface{} { - return r.MaxValue +// String returns a simple string for the range. +func (r Range) String() string { + return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) } // Translate maps a given value into the range space. // An example would be a 600 px image, with a min of 10 and a max of 100. // Translate(50) would yield (50.0/90.0)*600 ~= 333.33 -func (r RangeOfFloat64) Translate(value interface{}) int { - if typedValue, isTyped := value.(float64); isTyped { - finalValue := ((r.MaxValue - typedValue) / r.MinMaxDelta) * float64(r.Domain) - return int(math.Floor(finalValue)) - } - return 0 -} - -// NewRangeOfTime makes a new range of time with the given time values. -func NewRangeOfTime(domain int, values ...time.Time) Range { - min, max := MinAndMaxOfTime(values...) - r := &RangeOfTime{ - MinValue: min, - MaxValue: max, - MinMaxDelta: max.Unix() - min.Unix(), - Domain: domain, - } - return r -} - -// RangeOfTime represents a timeseries. -type RangeOfTime struct { - MinValue time.Time - MaxValue time.Time - MinMaxDelta int64 //unix time difference - Domain int -} - -// GetMin implements the interface method. -func (r RangeOfTime) GetMin() interface{} { - return r.MinValue -} - -// GetMax implements the interface method. -func (r RangeOfTime) GetMax() interface{} { - return r.MaxValue -} - -// Translate maps a given value into the range space (of time). -// An example would be a 600 px image, with a min of jan-01-2016 and a max of jun-01-2016. -// Translate(may-01-2016) would yield ... something. -func (r RangeOfTime) Translate(value interface{}) int { - if typed, isTyped := value.(time.Time); isTyped { - valueDelta := r.MaxValue.Unix() - typed.Unix() - finalValue := (float64(valueDelta) / float64(r.MinMaxDelta)) * float64(r.Domain) - return int(math.Floor(finalValue)) - } - return 0 +func (r Range) Translate(value float64) int { + finalValue := ((r.Max - value) / r.Delta()) * float64(r.Domain) + return int(math.Floor(finalValue)) } diff --git a/range_test.go b/range_test.go index 5cbb884..217730e 100644 --- a/range_test.go +++ b/range_test.go @@ -2,7 +2,6 @@ package chart import ( "testing" - "time" "github.com/blendlabs/go-assert" ) @@ -10,26 +9,7 @@ import ( func TestRangeTranslate(t *testing.T) { assert := assert.New(t) values := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} - r := NewRangeOfFloat64(1000, values...) - assert.Equal(1.0, r.GetMin()) - assert.Equal(8.0, r.GetMax()) + r := Range{Domain: 1000} + r.Min, r.Max = MinAndMax(values...) assert.Equal(428, r.Translate(5.0)) } - -func TestRangeOfTimeTranslate(t *testing.T) { - assert := assert.New(t) - values := []time.Time{ - time.Now().AddDate(0, 0, -1), - time.Now().AddDate(0, 0, -2), - time.Now().AddDate(0, 0, -3), - time.Now().AddDate(0, 0, -4), - time.Now().AddDate(0, 0, -5), - time.Now().AddDate(0, 0, -6), - time.Now().AddDate(0, 0, -7), - time.Now().AddDate(0, 0, -8), - } - r := NewRangeOfTime(1000, values...) - assert.Equal(values[7], r.GetMin()) - assert.Equal(values[0], r.GetMax()) - assert.Equal(571, r.Translate(time.Now().AddDate(0, 0, -5))) -} diff --git a/raster_renderer.go b/raster_renderer.go index 9c5fa36..524203c 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -1,6 +1,7 @@ package chart import ( + "fmt" "image" "image/color" "image/png" @@ -34,55 +35,60 @@ type rasterRenderer struct { // SetStrokeColor implements the interface method. func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) { - println("SetStrokeColor") + println("RasterRenderer :: SetStrokeColor", ColorAsString(c)) rr.gc.SetStrokeColor(c) } // SetFillColor implements the interface method. func (rr *rasterRenderer) SetFillColor(c color.RGBA) { - println("SetFillColor") + println("RasterRenderer :: SetFillColor", ColorAsString(c)) rr.gc.SetFillColor(c) } // SetLineWidth implements the interface method. -func (rr *rasterRenderer) SetLineWidth(width int) { - println("SetLineWidth", width) - rr.gc.SetLineWidth(float64(width)) +func (rr *rasterRenderer) SetLineWidth(width float64) { + println("RasterRenderer :: SetLineWidth", width) + rr.gc.SetLineWidth(width) } // MoveTo implements the interface method. func (rr *rasterRenderer) MoveTo(x, y int) { - println("MoveTo", x, y) + println("RasterRenderer :: MoveTo", x, y) rr.gc.MoveTo(float64(x), float64(y)) } // LineTo implements the interface method. func (rr *rasterRenderer) LineTo(x, y int) { - println("LineTo", x, y) + println("RasterRenderer :: LineTo", x, y) rr.gc.LineTo(float64(x), float64(y)) } // Close implements the interface method. func (rr *rasterRenderer) Close() { - println("Close") + println("RasterRenderer :: Close") rr.gc.Close() } // Stroke implements the interface method. func (rr *rasterRenderer) Stroke() { - println("Stroke") + println("RasterRenderer :: Stroke") rr.gc.Stroke() } +// Fill implements the interface method. +func (rr *rasterRenderer) Fill() { + println("RasterRenderer :: Fill") + rr.gc.Fill() +} + // FillStroke implements the interface method. func (rr *rasterRenderer) FillStroke() { - println("FillStroke") + println("RasterRenderer :: FillStroke") rr.gc.FillStroke() } // Circle implements the interface method. func (rr *rasterRenderer) Circle(radius float64, x, y int) { - println("Circle", radius, x, y) xf := float64(x) yf := float64(y) rr.gc.MoveTo(xf-radius, yf) //9 @@ -96,34 +102,32 @@ func (rr *rasterRenderer) Circle(radius float64, x, y int) { // SetFont implements the interface method. func (rr *rasterRenderer) SetFont(f *truetype.Font) { - println("SetFont") rr.font = f rr.gc.SetFont(f) } // SetFontSize implements the interface method. func (rr *rasterRenderer) SetFontSize(size float64) { - println("SetFontSize", size) + println("RasterRenderer :: SetFontSize", fmt.Sprintf("%.2f", size)) rr.fontSize = size rr.gc.SetFontSize(size) } // SetFontColor implements the interface method. func (rr *rasterRenderer) SetFontColor(c color.RGBA) { - println("SetFontColor") + println("RasterRenderer :: SetFontColor", ColorAsString(c)) rr.fontColor = c rr.gc.SetStrokeColor(c) } // Text implements the interface method. func (rr *rasterRenderer) Text(body string, x, y int) { - println("Text", body, x, y) + println("RasterRenderer :: Text", body, x, y) rr.gc.CreateStringPath(body, float64(x), float64(y)) } // MeasureText implements the interface method. func (rr *rasterRenderer) MeasureText(body string) int { - println("MeasureText", body) if rr.fc == nil && rr.font != nil { rr.fc = &font.Drawer{ Face: truetype.NewFace(rr.font, &truetype.Options{ @@ -141,6 +145,6 @@ func (rr *rasterRenderer) MeasureText(body string) int { // Save implements the interface method. func (rr *rasterRenderer) Save(w io.Writer) error { - println("Save") + println("RasterRenderer :: Save") return png.Encode(w, rr.i) } diff --git a/renderer.go b/renderer.go index 902a8ed..84371cb 100644 --- a/renderer.go +++ b/renderer.go @@ -19,7 +19,7 @@ type Renderer interface { SetFillColor(color.RGBA) // SetLineWidth sets the stroke line width. - SetLineWidth(width int) + SetLineWidth(width float64) // MoveTo moves the cursor to a given point. MoveTo(x, y int) @@ -31,10 +31,13 @@ type Renderer interface { // Close finalizes a shape as drawn by LineTo. Close() - // Stroke draws the 'stroke' or line component of a shape. + // Stroke strokes the path. Stroke() - // FillStroke draws the 'stroke' and 'fills' a shape. + // Fill fills the path, but does not stroke. + Fill() + + // FillStroke fills and strokes a path. FillStroke() // Circle draws a circle at the given coords with a given radius. diff --git a/series.go b/series.go index 7575629..a9d0bc8 100644 --- a/series.go +++ b/series.go @@ -1,16 +1,17 @@ package chart -import "time" +import ( + "fmt" + "time" +) // Series is a entity data set. type Series interface { GetName() string GetStyle() Style Len() int - GetValue(index int) (interface{}, float64) - - GetXRange(domain int) Range - GetYRange(domain int) Range + GetValue(index int) (float64, float64) + GetLabel(index int) (string, string) } // TimeSeries is a line on a chart. @@ -37,19 +38,18 @@ func (ts TimeSeries) Len() int { return len(ts.XValues) } -// GetXRange returns the x range. -func (ts TimeSeries) GetXRange(domain int) Range { - return NewRangeOfTime(domain, ts.XValues...) -} - -// GetYRange returns the x range. -func (ts TimeSeries) GetYRange(domain int) Range { - return NewRangeOfFloat64(domain, ts.YValues...) -} - // GetValue gets a value at a given index. -func (ts TimeSeries) GetValue(index int) (interface{}, float64) { - return ts.XValues[index], ts.YValues[index] +func (ts TimeSeries) GetValue(index int) (x float64, y float64) { + x = float64(ts.XValues[index].Unix()) + y = ts.YValues[index] + return +} + +// GetLabel gets a label for the values at a given index. +func (ts TimeSeries) GetLabel(index int) (xLabel string, yLabel string) { + xLabel = ts.XValues[index].Format(DefaultDateFormat) + yLabel = fmt.Sprintf("%0.2f", ts.YValues[index]) + return } // ContinousSeries represents a line on a chart. @@ -81,12 +81,9 @@ func (cs ContinousSeries) GetValue(index int) (interface{}, float64) { return cs.XValues[index], cs.YValues[index] } -// GetXRange returns the x range. -func (cs ContinousSeries) GetXRange(domain int) Range { - return NewRangeOfFloat64(domain, cs.XValues...) -} - -// GetYRange returns the x range. -func (cs ContinousSeries) GetYRange(domain int) Range { - return NewRangeOfFloat64(domain, cs.YValues...) +// GetLabel gets a label for the values at a given index. +func (cs ContinousSeries) GetLabel(index int) (xLabel string, yLabel string) { + xLabel = fmt.Sprintf("%0.2f", cs.XValues[index]) + yLabel = fmt.Sprintf("%0.2f", cs.YValues[index]) + return } diff --git a/series_test.go b/series_test.go index 01ff492..3194997 100644 --- a/series_test.go +++ b/series_test.go @@ -28,29 +28,3 @@ func TestTimeSeriesGetValue(t *testing.T) { assert.NotZero(x0) assert.Equal(1.0, y0) } - -func TestTimeSeriesRanges(t *testing.T) { - assert := assert.New(t) - - ts := TimeSeries{ - Name: "Test", - XValues: []time.Time{ - time.Now().AddDate(0, 0, -5), - time.Now().AddDate(0, 0, -4), - time.Now().AddDate(0, 0, -3), - time.Now().AddDate(0, 0, -2), - time.Now().AddDate(0, 0, -1), - }, - YValues: []float64{ - 1.0, 2.0, 3.0, 4.0, 5.0, - }, - } - - xrange := ts.GetXRange(1000) - x0 := xrange.Translate(time.Now().AddDate(0, 0, -3)) - assert.Equal(500, x0) - - yrange := ts.GetYRange(400) - y0 := yrange.Translate(3.0) - assert.Equal(200, y0) -} diff --git a/style.go b/style.go index 6f241d3..8a49fd4 100644 --- a/style.go +++ b/style.go @@ -4,36 +4,70 @@ import "image/color" // Style is a simple style set. type Style struct { + Show bool StrokeColor color.RGBA FillColor color.RGBA - StrokeWidth int + StrokeWidth float64 + FontSize float64 + FontColor color.RGBA } // IsZero returns if the object is set or not. func (s Style) IsZero() bool { - return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0 + return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0 && s.FontSize == 0 } // GetStrokeColor returns the stroke color. -func (s Style) GetStrokeColor() color.RGBA { +func (s Style) GetStrokeColor(defaults ...color.RGBA) color.RGBA { if ColorIsZero(s.StrokeColor) { + if len(defaults) > 0 { + return defaults[0] + } return DefaultLineColor } return s.StrokeColor } // GetFillColor returns the fill color. -func (s Style) GetFillColor() color.RGBA { +func (s Style) GetFillColor(defaults ...color.RGBA) color.RGBA { if ColorIsZero(s.FillColor) { + if len(defaults) > 0 { + return defaults[0] + } return DefaultFillColor } return s.FillColor } // GetStrokeWidth returns the stroke width. -func (s Style) GetStrokeWidth() int { +func (s Style) GetStrokeWidth(defaults ...float64) float64 { if s.StrokeWidth == 0 { + if len(defaults) > 0 { + return defaults[0] + } return DefaultLineWidth } return s.StrokeWidth } + +// GetFontSize gets the font size. +func (s Style) GetFontSize(defaults ...float64) float64 { + if s.FontSize == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return DefaultFontSize + } + return s.FontSize +} + +// GetFontColor gets the font size. +func (s Style) GetFontColor(defaults ...color.RGBA) color.RGBA { + if ColorIsZero(s.FontColor) { + if len(defaults) > 0 { + return defaults[0] + } + return DefaultTextColor + } + return s.FontColor +} diff --git a/testserver/main.go b/testserver/main.go new file mode 100644 index 0000000..d5ac454 --- /dev/null +++ b/testserver/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "bytes" + "log" + "time" + + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-web" +) + +func main() { + app := web.New() + app.SetName("Chart Test Server") + app.SetLogger(web.NewStandardOutputLogger()) + app.GET("/", func(rc *web.RequestContext) web.ControllerResult { + rc.Response.Header().Set("Content-Type", "image/png") + now := time.Now() + c := chart.Chart{ + Title: "Hello!", + TitleStyle: chart.Style{ + Show: true, + }, + Width: 1024, + Height: 400, + Padding: chart.Box{ + Right: 40, + Bottom: 40, + }, + Axes: chart.Style{ + Show: true, + StrokeWidth: 1.0, + }, + YRange: chart.Range{ + Min: 0.0, + Max: 7.0, + }, + Series: []chart.Series{ + chart.TimeSeries{ + Name: "goog", + Style: chart.Style{ + StrokeWidth: 1.0, + }, + XValues: []time.Time{now.AddDate(0, 0, -4), now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)}, + YValues: []float64{2.5, 5.0, 2.0, 3.0}, + }, + }, + } + + buffer := bytes.NewBuffer([]byte{}) + err := c.Render(chart.PNG, buffer) + if err != nil { + return rc.API().InternalError(err) + } + return rc.Raw(buffer.Bytes()) + }) + log.Fatal(app.Start()) +} diff --git a/util.go b/util.go index 8876e91..1b6a6bd 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,7 @@ package chart import ( + "fmt" "image/color" "time" ) @@ -10,6 +11,11 @@ func ColorIsZero(c color.RGBA) bool { return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 } +// ColorAsString returns if a color.RGBA is unset or not. +func ColorAsString(c color.RGBA) string { + return fmt.Sprintf("RGBA(%v,%v,%v,%v)", c.R, c.G, c.G, c.A) +} + // MinAndMax returns both the min and max in one pass. func MinAndMax(values ...float64) (min float64, max float64) { if len(values) == 0 {