diff --git a/examples/annotations/main.go b/_examples/annotations/main.go similarity index 100% rename from examples/annotations/main.go rename to _examples/annotations/main.go diff --git a/examples/axes/main.go b/_examples/axes/main.go similarity index 100% rename from examples/axes/main.go rename to _examples/axes/main.go diff --git a/examples/bar_chart/main.go b/_examples/bar_chart/main.go similarity index 100% rename from examples/bar_chart/main.go rename to _examples/bar_chart/main.go diff --git a/examples/basic/main.go b/_examples/basic/main.go similarity index 100% rename from examples/basic/main.go rename to _examples/basic/main.go diff --git a/examples/custom_formatters/main.go b/_examples/custom_formatters/main.go similarity index 100% rename from examples/custom_formatters/main.go rename to _examples/custom_formatters/main.go diff --git a/examples/custom_padding/main.go b/_examples/custom_padding/main.go similarity index 100% rename from examples/custom_padding/main.go rename to _examples/custom_padding/main.go diff --git a/examples/custom_ranges/main.go b/_examples/custom_ranges/main.go similarity index 100% rename from examples/custom_ranges/main.go rename to _examples/custom_ranges/main.go diff --git a/examples/custom_styles/main.go b/_examples/custom_styles/main.go similarity index 100% rename from examples/custom_styles/main.go rename to _examples/custom_styles/main.go diff --git a/examples/custom_ticks/main.go b/_examples/custom_ticks/main.go similarity index 100% rename from examples/custom_ticks/main.go rename to _examples/custom_ticks/main.go diff --git a/examples/legend/main.go b/_examples/legend/main.go similarity index 100% rename from examples/legend/main.go rename to _examples/legend/main.go diff --git a/examples/linear_regression/main.go b/_examples/linear_regression/main.go similarity index 100% rename from examples/linear_regression/main.go rename to _examples/linear_regression/main.go diff --git a/examples/market_hours/main.go b/_examples/market_hours/main.go similarity index 100% rename from examples/market_hours/main.go rename to _examples/market_hours/main.go diff --git a/examples/min_max/main.go b/_examples/min_max/main.go similarity index 80% rename from examples/min_max/main.go rename to _examples/min_max/main.go index 056b1fd..2376efe 100644 --- a/examples/min_max/main.go +++ b/_examples/min_max/main.go @@ -32,8 +32,12 @@ func drawChart(res http.ResponseWriter, req *http.Request) { } graph := chart.Chart{ + Width: 1920, + Height: 1080, YAxis: chart.YAxis{ - Style: chart.StyleShow(), + Name: "Random Values", + NameStyle: chart.StyleShow(), + Style: chart.StyleShow(), Range: &chart.ContinuousRange{ Min: 25, Max: 175, @@ -51,8 +55,10 @@ func drawChart(res http.ResponseWriter, req *http.Request) { }, } - res.Header().Set("Content-Type", "image/png") - graph.Render(chart.PNG, res) + graph.Elements = []chart.Renderable{chart.Legend(&graph)} + + res.Header().Set("Content-Type", "image/svg+xml") + graph.Render(chart.SVG, res) } func main() { diff --git a/examples/pie_chart/main.go b/_examples/pie_chart/main.go similarity index 100% rename from examples/pie_chart/main.go rename to _examples/pie_chart/main.go diff --git a/examples/response_times/main.go b/_examples/response_times/main.go similarity index 100% rename from examples/response_times/main.go rename to _examples/response_times/main.go diff --git a/examples/simple_moving_average/main.go b/_examples/simple_moving_average/main.go similarity index 100% rename from examples/simple_moving_average/main.go rename to _examples/simple_moving_average/main.go diff --git a/examples/stacked_bar/main.go b/_examples/stacked_bar/main.go similarity index 100% rename from examples/stacked_bar/main.go rename to _examples/stacked_bar/main.go diff --git a/examples/stock_analysis/main.go b/_examples/stock_analysis/main.go similarity index 100% rename from examples/stock_analysis/main.go rename to _examples/stock_analysis/main.go diff --git a/examples/timeseries/main.go b/_examples/timeseries/main.go similarity index 100% rename from examples/timeseries/main.go rename to _examples/timeseries/main.go diff --git a/examples/twoaxis/main.go b/_examples/twoaxis/main.go similarity index 100% rename from examples/twoaxis/main.go rename to _examples/twoaxis/main.go diff --git a/images/bar_chart.png b/_images/bar_chart.png similarity index 100% rename from images/bar_chart.png rename to _images/bar_chart.png diff --git a/images/goog_ltm.png b/_images/goog_ltm.png similarity index 100% rename from images/goog_ltm.png rename to _images/goog_ltm.png diff --git a/images/ma_goog_ltm.png b/_images/ma_goog_ltm.png similarity index 100% rename from images/ma_goog_ltm.png rename to _images/ma_goog_ltm.png diff --git a/images/pie_chart.png b/_images/pie_chart.png similarity index 100% rename from images/pie_chart.png rename to _images/pie_chart.png diff --git a/images/spy_ltm_bbs.png b/_images/spy_ltm_bbs.png similarity index 100% rename from images/spy_ltm_bbs.png rename to _images/spy_ltm_bbs.png diff --git a/images/stacked_bar.png b/_images/stacked_bar.png similarity index 100% rename from images/stacked_bar.png rename to _images/stacked_bar.png diff --git a/images/tvix_ltm.png b/_images/tvix_ltm.png similarity index 100% rename from images/tvix_ltm.png rename to _images/tvix_ltm.png diff --git a/images/two_axis.png b/_images/two_axis.png similarity index 100% rename from images/two_axis.png rename to _images/two_axis.png diff --git a/drawing/matrix.go b/drawing/matrix.go index 32e0ef4..8bd2049 100644 --- a/drawing/matrix.go +++ b/drawing/matrix.go @@ -161,10 +161,10 @@ func (tr *Matrix) Translate(tx, ty float64) { tr[5] = ty*tr[3] + tx*tr[1] + tr[5] } -// Rotate adds a rotation to the matrix. angle is in radian -func (tr *Matrix) Rotate(angle float64) { - c := math.Cos(angle) - s := math.Sin(angle) +// Rotate adds a rotation to the matrix. +func (tr *Matrix) Rotate(radians float64) { + c := math.Cos(radians) + s := math.Sin(radians) t0 := c*tr[0] + s*tr[2] t1 := s*tr[3] + c*tr[1] t2 := c*tr[2] - s*tr[0] diff --git a/drawing/text.go b/drawing/text.go index e1b40f2..d7ff5c2 100644 --- a/drawing/text.go +++ b/drawing/text.go @@ -22,14 +22,10 @@ func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) { } else { path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) } - } else { - if on0 { - // No-op. - } else { - midX := (q0X + qX) / 2 - midY := (q0Y + qY) / 2 - path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) - } + } else if !on0 { + midX := (q0X + qX) / 2 + midY := (q0Y + qY) / 2 + path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) } q0X, q0Y, on0 = qX, qY, on } diff --git a/raster_renderer.go b/raster_renderer.go index 347bb64..3b9fa4d 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -28,6 +28,8 @@ type rasterRenderer struct { i *image.RGBA gc *drawing.RasterGraphicContext + rotateRadians float64 + s Style } @@ -139,10 +141,11 @@ func (rr *rasterRenderer) SetFontColor(c drawing.Color) { // Text implements the interface method. func (rr *rasterRenderer) Text(body string, x, y int) { + xf, yf := rr.getCoords(x, y) rr.gc.SetFont(rr.s.Font) rr.gc.SetFontSize(rr.s.FontSize) rr.gc.SetFillColor(rr.s.FontColor) - rr.gc.CreateStringPath(body, float64(x), float64(y)) + rr.gc.CreateStringPath(body, float64(xf), float64(yf)) rr.gc.Fill() } @@ -182,6 +185,29 @@ func (rr *rasterRenderer) MeasureText(body string) Box { } } +// SetTextRotation sets a text rotation. +func (rr *rasterRenderer) SetTextRotation(radians float64) { + rr.rotateRadians = radians +} + +func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) { + if rr.rotateRadians == 0 { + xf = x + yf = y + return + } + + rr.gc.Translate(float64(x), float64(y)) + rr.gc.Rotate(rr.rotateRadians) + return +} + +// ClearTextRotation clears text rotation. +func (rr *rasterRenderer) ClearTextRotation() { + rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix()) + rr.rotateRadians = 0 +} + // Save implements the interface method. func (rr *rasterRenderer) Save(w io.Writer) error { return png.Encode(w, rr.i) diff --git a/renderer.go b/renderer.go index 9d4a9ea..2047a40 100644 --- a/renderer.go +++ b/renderer.go @@ -72,6 +72,12 @@ type Renderer interface { // MeasureText measures text. MeasureText(body string) Box + // SetTextRotatation sets a rotation for drawing elements. + SetTextRotation(radians float64) + + // ClearTextRotation clears rotation. + ClearTextRotation() + // Save writes the image to the given writer. Save(w io.Writer) error } diff --git a/vector_renderer.go b/vector_renderer.go index 7e50dc8..d65e497 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -32,6 +32,7 @@ type vectorRenderer struct { b *bytes.Buffer c *canvas s *Style + r float64 p []string fc *font.Drawer } @@ -171,6 +172,16 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) { return } +// SetTextRotation sets the text rotation. +func (vr *vectorRenderer) SetTextRotation(radians float64) { + vr.c.r = radians +} + +// ClearTextRotation clears the text rotation. +func (vr *vectorRenderer) ClearTextRotation() { + vr.c.r = 0 +} + // Save saves the renderer's contents to a writer. func (vr *vectorRenderer) Save(w io.Writer) error { vr.c.End() @@ -187,6 +198,7 @@ func newCanvas(w io.Writer) *canvas { type canvas struct { w io.Writer dpi float64 + r float64 width int height int } @@ -206,7 +218,12 @@ func (c *canvas) Path(d string, style Style) { } func (c *canvas) Text(x, y int, body string, style Style) { - c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) + if c.r == 0 { + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body))) + } else { + transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, Math.RadiansToDegrees(c.r), x, y) + c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body))) + } } func (c *canvas) Circle(x, y, r int, style Style) { diff --git a/yaxis.go b/yaxis.go index 7012e21..5e8a2c6 100644 --- a/yaxis.go +++ b/yaxis.go @@ -8,7 +8,9 @@ import ( // YAxis is a veritcal rule of the range. // There can be (2) y-axes; a primary and secondary. type YAxis struct { - Name string + Name string + NameStyle Style + Style Style Zero GridLine @@ -30,6 +32,11 @@ func (ya YAxis) GetName() string { return ya.Name } +// GetNameStyle returns the name style. +func (ya YAxis) GetNameStyle() Style { + return ya.NameStyle +} + // GetStyle returns the style. func (ya YAxis) GetStyle() Style { return ya.Style @@ -73,6 +80,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic } var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0 + var maxTextHeight int for _, t := range ticks { v := t.Value ly := canvasBox.Bottom - ra.Translate(v) @@ -83,6 +91,10 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic finalTextX = tx - tb.Width() } + if tb.Height() > maxTextHeight { + maxTextHeight = tb.Height() + } + if ya.AxisType == YAxisPrimary { minx = canvasBox.Right maxx = Math.MaxInt(maxx, tx+tb.Width()) @@ -94,6 +106,10 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic maxy = Math.MaxInt(maxy, ly+tb.Height()>>1) } + if ya.NameStyle.Show && len(ya.Name) > 0 { + maxx += (DefaultYAxisMargin + maxTextHeight) + } + return Box{ Top: miny, Left: minx, @@ -124,12 +140,17 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick r.LineTo(lx, canvasBox.Top) r.Stroke() + var maxTextWidth int for _, t := range ticks { v := t.Value ly := canvasBox.Bottom - ra.Translate(v) tb := r.MeasureText(t.Label) + if tb.Width() > maxTextWidth { + maxTextWidth = tb.Width() + } + finalTextX := tx finalTextY := ly + tb.Height()>>1 if ya.AxisType == YAxisSecondary { @@ -147,8 +168,18 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick r.Stroke() } - if ya.Zero.Style.Show { - ya.Zero.Render(r, canvasBox, ra, Style{}) + nameStyle := ya.NameStyle.InheritFrom(defaults) + if ya.NameStyle.Show && len(ya.Name) > 0 { + nameStyle.GetTextOptions().WriteToRenderer(r) + + r.SetTextRotation(Math.DegreesToRadians(90)) + + tb := r.MeasureText(ya.Name) + tx := canvasBox.Right + int(sw) + DefaultYAxisMargin + maxTextWidth + DefaultYAxisMargin + ty := canvasBox.Bottom - (canvasBox.Height()>>1 + tb.Width()>>1) + + r.Text(ya.Name, tx, ty) + r.ClearTextRotation() } if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show {