diff --git a/defaults.go b/defaults.go index d74330fd..ceb9645f 100644 --- a/defaults.go +++ b/defaults.go @@ -58,6 +58,11 @@ const ( // DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks. DefaultMinimumTickVerticalSpacing = 20 + // DefaultLegendHorizontalSpacing is the minimum distance between two horizontal elements in the legend. + DefaultLegendHorizontalSpacing = 20 + // DefaultLegendVerticalSpacing is the minimum distance between two vertical elements in the legend. + DefaultLegendVerticalSpacing = 10 + // DefaultDateFormat is the default date format. DefaultDateFormat = "2006-01-02" // DefaultDateHourFormat is the date format for hour timestamp formats. diff --git a/examples/legend_line_left/main.go b/examples/legend_line_left/main.go new file mode 100644 index 00000000..10675ccd --- /dev/null +++ b/examples/legend_line_left/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "github.com/wcharczuk/go-chart/v2" + "os" + "time" +) + +func main() { + xv, yv := xvalues(), yvalues() + + priceSeries := chart.TimeSeries{ + Name: "Very long name for a time series to test legend", + Style: chart.Style{ + StrokeColor: chart.GetDefaultColor(0), + }, + XValues: xv, + YValues: yv, + } + + linRegSeries := &chart.LinearRegressionSeries{ + Name: "Regression", + InnerSeries: priceSeries, + } + + graph := chart.Chart{ + XAxis: chart.XAxis{ + TickPosition: chart.TickPositionBetweenTicks, + }, + YAxis: chart.YAxis{ + Range: &chart.ContinuousRange{ + Max: 220.0, + Min: 180.0, + }, + }, + Series: []chart.Series{ + priceSeries, + linRegSeries, + }, + } + + graph.Elements = []chart.Renderable{chart.LegendLineLeft(&graph, chart.Style{})} + + f, _ := os.Create("output.png") + defer f.Close() + graph.Render(chart.PNG, f) +} + +func xvalues() []time.Time { + rawx := []string{"2015-07-17", "2015-07-20", "2015-07-21", "2015-07-22", "2015-07-23", "2015-07-24", "2015-07-27", "2015-07-28", "2015-07-29", "2015-07-30", "2015-07-31", "2015-08-03", "2015-08-04", "2015-08-05", "2015-08-06", "2015-08-07", "2015-08-10", "2015-08-11", "2015-08-12", "2015-08-13", "2015-08-14", "2015-08-17", "2015-08-18", "2015-08-19", "2015-08-20", "2015-08-21", "2015-08-24", "2015-08-25", "2015-08-26", "2015-08-27", "2015-08-28", "2015-08-31", "2015-09-01", "2015-09-02", "2015-09-03", "2015-09-04", "2015-09-08", "2015-09-09", "2015-09-10", "2015-09-11", "2015-09-14", "2015-09-15", "2015-09-16", "2015-09-17", "2015-09-18", "2015-09-21", "2015-09-22", "2015-09-23", "2015-09-24", "2015-09-25", "2015-09-28", "2015-09-29", "2015-09-30", "2015-10-01", "2015-10-02", "2015-10-05", "2015-10-06", "2015-10-07", "2015-10-08", "2015-10-09", "2015-10-12", "2015-10-13", "2015-10-14", "2015-10-15", "2015-10-16", "2015-10-19", "2015-10-20", "2015-10-21", "2015-10-22", "2015-10-23", "2015-10-26", "2015-10-27", "2015-10-28", "2015-10-29", "2015-10-30", "2015-11-02", "2015-11-03", "2015-11-04", "2015-11-05", "2015-11-06", "2015-11-09", "2015-11-10", "2015-11-11", "2015-11-12", "2015-11-13", "2015-11-16", "2015-11-17", "2015-11-18", "2015-11-19", "2015-11-20", "2015-11-23", "2015-11-24", "2015-11-25", "2015-11-27", "2015-11-30", "2015-12-01", "2015-12-02", "2015-12-03", "2015-12-04", "2015-12-07", "2015-12-08", "2015-12-09", "2015-12-10", "2015-12-11", "2015-12-14", "2015-12-15", "2015-12-16", "2015-12-17", "2015-12-18", "2015-12-21", "2015-12-22", "2015-12-23", "2015-12-24", "2015-12-28", "2015-12-29", "2015-12-30", "2015-12-31", "2016-01-04", "2016-01-05", "2016-01-06", "2016-01-07", "2016-01-08", "2016-01-11", "2016-01-12", "2016-01-13", "2016-01-14", "2016-01-15", "2016-01-19", "2016-01-20", "2016-01-21", "2016-01-22", "2016-01-25", "2016-01-26", "2016-01-27", "2016-01-28", "2016-01-29", "2016-02-01", "2016-02-02", "2016-02-03", "2016-02-04", "2016-02-05", "2016-02-08", "2016-02-09", "2016-02-10", "2016-02-11", "2016-02-12", "2016-02-16", "2016-02-17", "2016-02-18", "2016-02-19", "2016-02-22", "2016-02-23", "2016-02-24", "2016-02-25", "2016-02-26", "2016-02-29", "2016-03-01", "2016-03-02", "2016-03-03", "2016-03-04", "2016-03-07", "2016-03-08", "2016-03-09", "2016-03-10", "2016-03-11", "2016-03-14", "2016-03-15", "2016-03-16", "2016-03-17", "2016-03-18", "2016-03-21", "2016-03-22", "2016-03-23", "2016-03-24", "2016-03-28", "2016-03-29", "2016-03-30", "2016-03-31", "2016-04-01", "2016-04-04", "2016-04-05", "2016-04-06", "2016-04-07", "2016-04-08", "2016-04-11", "2016-04-12", "2016-04-13", "2016-04-14", "2016-04-15", "2016-04-18", "2016-04-19", "2016-04-20", "2016-04-21", "2016-04-22", "2016-04-25", "2016-04-26", "2016-04-27", "2016-04-28", "2016-04-29", "2016-05-02", "2016-05-03", "2016-05-04", "2016-05-05", "2016-05-06", "2016-05-09", "2016-05-10", "2016-05-11", "2016-05-12", "2016-05-13", "2016-05-16", "2016-05-17", "2016-05-18", "2016-05-19", "2016-05-20", "2016-05-23", "2016-05-24", "2016-05-25", "2016-05-26", "2016-05-27", "2016-05-31", "2016-06-01", "2016-06-02", "2016-06-03", "2016-06-06", "2016-06-07", "2016-06-08", "2016-06-09", "2016-06-10", "2016-06-13", "2016-06-14", "2016-06-15", "2016-06-16", "2016-06-17", "2016-06-20", "2016-06-21", "2016-06-22", "2016-06-23", "2016-06-24", "2016-06-27", "2016-06-28", "2016-06-29", "2016-06-30", "2016-07-01", "2016-07-05", "2016-07-06", "2016-07-07", "2016-07-08", "2016-07-11", "2016-07-12", "2016-07-13", "2016-07-14", "2016-07-15"} + + var dates []time.Time + for _, ts := range rawx { + parsed, _ := time.Parse(chart.DefaultDateFormat, ts) + dates = append(dates, parsed) + } + return dates +} + +func yvalues() []float64 { + return []float64{212.47, 212.59, 211.76, 211.37, 210.18, 208.00, 206.79, 209.33, 210.77, 210.82, 210.50, 209.79, 209.38, 210.07, 208.35, 207.95, 210.57, 208.66, 208.92, 208.66, 209.42, 210.59, 209.98, 208.32, 203.97, 197.83, 189.50, 187.27, 194.46, 199.27, 199.28, 197.67, 191.77, 195.41, 195.55, 192.59, 197.43, 194.79, 195.85, 196.74, 196.01, 198.45, 200.18, 199.73, 195.45, 196.46, 193.90, 193.60, 192.90, 192.87, 188.01, 188.12, 191.63, 192.13, 195.00, 198.47, 197.79, 199.41, 201.21, 201.33, 201.52, 200.25, 199.29, 202.35, 203.27, 203.37, 203.11, 201.85, 205.26, 207.51, 207.00, 206.60, 208.95, 208.83, 207.93, 210.39, 211.00, 210.36, 210.15, 210.04, 208.08, 208.56, 207.74, 204.84, 202.54, 205.62, 205.47, 208.73, 208.55, 209.31, 209.07, 209.35, 209.32, 209.56, 208.69, 210.68, 208.53, 205.61, 209.62, 208.35, 206.95, 205.34, 205.87, 201.88, 202.90, 205.03, 208.03, 204.86, 200.02, 201.67, 203.50, 206.02, 205.68, 205.21, 207.40, 205.93, 203.87, 201.02, 201.36, 198.82, 194.05, 191.92, 192.11, 193.66, 188.83, 191.93, 187.81, 188.06, 185.65, 186.69, 190.52, 187.64, 190.20, 188.13, 189.11, 193.72, 193.65, 190.16, 191.30, 191.60, 187.95, 185.42, 185.43, 185.27, 182.86, 186.63, 189.78, 192.88, 192.09, 192.00, 194.78, 192.32, 193.20, 195.54, 195.09, 193.56, 198.11, 199.00, 199.78, 200.43, 200.59, 198.40, 199.38, 199.54, 202.76, 202.50, 202.17, 203.34, 204.63, 204.38, 204.67, 204.56, 203.21, 203.12, 203.24, 205.12, 206.02, 205.52, 206.92, 206.25, 204.19, 206.42, 203.95, 204.50, 204.02, 205.92, 208.00, 208.01, 207.78, 209.24, 209.90, 210.10, 208.97, 208.97, 208.61, 208.92, 209.35, 207.45, 206.33, 207.97, 206.16, 205.01, 204.97, 205.72, 205.89, 208.45, 206.50, 206.56, 204.76, 206.78, 204.85, 204.91, 204.20, 205.49, 205.21, 207.87, 209.28, 209.34, 210.24, 209.84, 210.27, 210.91, 210.28, 211.35, 211.68, 212.37, 212.08, 210.07, 208.45, 208.04, 207.75, 208.37, 206.52, 207.85, 208.44, 208.10, 210.81, 203.24, 199.60, 203.20, 206.66, 209.48, 209.92, 208.41, 209.66, 209.53, 212.65, 213.40, 214.95, 214.92, 216.12, 215.83} +} diff --git a/examples/legend_line_left/output.png b/examples/legend_line_left/output.png new file mode 100644 index 00000000..e6b893cc Binary files /dev/null and b/examples/legend_line_left/output.png differ diff --git a/legend.go b/legend.go index fbd48ed0..7f1c13f6 100644 --- a/legend.go +++ b/legend.go @@ -13,6 +13,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { FontSize: 8.0, StrokeColor: DefaultAxisColor, StrokeWidth: DefaultAxisLineWidth, + Padding: Box{ + Top: 5, + Left: 5, + Right: 5, + Bottom: 5, + }, } var legendStyle Style @@ -23,12 +29,6 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { } // DEFAULTS - legendPadding := Box{ - Top: 5, - Left: 5, - Right: 5, - Bottom: 5, - } lineTextGap := 5 lineLengthMinimum := 25 @@ -50,10 +50,10 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { } legendContent := Box{ - Top: legend.Top + legendPadding.Top, - Left: legend.Left + legendPadding.Left, - Right: legend.Left + legendPadding.Left, - Bottom: legend.Top + legendPadding.Top, + Top: legend.Top + legendStyle.Padding.Top, + Left: legend.Left + legendStyle.Padding.Left, + Right: legend.Left + legendStyle.Padding.Left, + Bottom: legend.Top + legendStyle.Padding.Top, } legendStyle.GetTextOptions().WriteToRenderer(r) @@ -64,7 +64,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { if len(labels[x]) > 0 { tb := r.MeasureText(labels[x]) if labelCount > 0 { - legendContent.Bottom += DefaultMinimumTickVerticalSpacing + legendContent.Bottom += DefaultLegendVerticalSpacing } legendContent.Bottom += tb.Height() right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum @@ -74,8 +74,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { } legend = legend.Grow(legendContent) - legend.Right = legendContent.Right + legendPadding.Right - legend.Bottom = legendContent.Bottom + legendPadding.Bottom + legend.Right = legendContent.Right + legendStyle.Padding.Right + legend.Bottom = legendContent.Bottom + legendStyle.Padding.Bottom Draw.Box(r, legend, legendStyle) @@ -89,7 +89,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { label = labels[x] if len(label) > 0 { if legendCount > 0 { - ycursor += DefaultMinimumTickVerticalSpacing + ycursor += DefaultLegendVerticalSpacing } tb := r.MeasureText(label) @@ -101,7 +101,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { lx := tx + tb.Width() + lineTextGap ly := ty - th2 - lx2 := legendContent.Right - legendPadding.Right + lx2 := legendContent.Right - legendStyle.Padding.Right r.SetStrokeColor(lines[x].GetStrokeColor()) r.SetStrokeWidth(lines[x].GetStrokeWidth()) @@ -118,6 +118,125 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { } } +// LegendLineLeft is a legend with the line drawn left to the legend text. +func LegendLineLeft(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, + Padding: Box{ + Top: 5, + Left: 5, + Right: 5, + Bottom: 5, + }, + } + + var legendStyle Style + if len(userDefaults) > 0 { + legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults)) + } else { + legendStyle = chartDefaults.InheritFrom(legendDefaults) + } + + // DEFAULTS + lineTextGap := 5 + lineLengthMinimum := 25 + strokeLength := 17 + + var labels []string + var lines []Style + for index, s := range c.Series { + if !s.GetStyle().Hidden { + if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { + labels = append(labels, s.GetName()) + lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) + } + } + } + + legend := Box{ + Top: cb.Top, + Left: cb.Left, + // bottom and right will be sized by the legend content + relevant padding. + } + + legendContent := Box{ + Top: legend.Top + legendStyle.Padding.Top, + Left: legend.Left + legendStyle.Padding.Left, + Right: legend.Left + legendStyle.Padding.Left, + Bottom: legend.Top + legendStyle.Padding.Top, + } + + legendStyle.GetTextOptions().WriteToRenderer(r) + + // measure + labelCount := 0 + for x := 0; x < len(labels); x++ { + if len(labels[x]) > 0 { + tb := r.MeasureText(labels[x]) + if labelCount > 0 { + legendContent.Bottom += DefaultLegendVerticalSpacing + } + legendContent.Bottom += tb.Height() + right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum + legendContent.Right = MaxInt(legendContent.Right, right) + labelCount++ + } + } + + legend = legend.Grow(legendContent) + legend.Right = legendContent.Right + legendStyle.Padding.Right + legend.Bottom = legendContent.Bottom + legendStyle.Padding.Bottom + + Draw.Box(r, legend, legendStyle) + + legendStyle.GetTextOptions().WriteToRenderer(r) + + ycursor := legendContent.Top + lx := legendContent.Left + legendCount := 0 + var label string + for x := 0; x < len(labels); x++ { + label = labels[x] + if len(label) > 0 { + if legendCount > 0 { + ycursor += DefaultLegendVerticalSpacing + } + + // Calculate text dimensions + tb := r.MeasureText(label) + ty := ycursor + tb.Height() + th2 := tb.Height() >> 1 + + // Calculate line x and y coordinates + ly := ty - th2 + + // Calculate line ending x coordinate + lx2 := lx + strokeLength + + r.SetStrokeColor(lines[x].GetStrokeColor()) + r.SetStrokeWidth(lines[x].GetStrokeWidth()) + r.SetStrokeDashArray(lines[x].GetStrokeDashArray()) + + r.MoveTo(lx, ly) + r.LineTo(lx2, ly) + r.Stroke() + + // Calculate Text starting coordinates + textX := lx2 + lineTextGap + r.Text(label, textX, ty) + + ycursor += tb.Height() + legendCount++ + } + } + } +} + // LegendThin is a legend that doesn't obscure the chart area. func LegendThin(c *Chart, userDefaults ...Style) Renderable { return func(r Renderer, cb Box, chartDefaults Style) { @@ -179,7 +298,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { Bottom: legendYMargin + legendBoxHeight, } - Draw.Box(r, legendBox, legendDefaults) + Draw.Box(r, legendBox, legendStyle) r.SetFont(legendStyle.GetFont()) r.SetFontColor(legendStyle.GetFontColor()) @@ -210,7 +329,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { r.LineTo(lx+lineLengthMinimum, ly) r.Stroke() - tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum + tx += textBox.Width() + DefaultLegendHorizontalSpacing + lineTextGap + lineLengthMinimum } } } @@ -225,6 +344,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { FontSize: 8.0, StrokeColor: DefaultAxisColor, StrokeWidth: DefaultAxisLineWidth, + Padding: Box{ + Top: 5, + Left: 5, + Right: 5, + Bottom: 5, + }, } var legendStyle Style @@ -235,12 +360,6 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { } // DEFAULTS - legendPadding := Box{ - Top: 5, - Left: 5, - Right: 5, - Bottom: 5, - } lineTextGap := 5 lineLengthMinimum := 25 @@ -262,10 +381,10 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { } legendContent := Box{ - Top: legend.Top + legendPadding.Top, - Left: legend.Left + legendPadding.Left, - Right: legend.Left + legendPadding.Left, - Bottom: legend.Top + legendPadding.Top, + Top: legend.Top + legendStyle.Padding.Top, + Left: legend.Left + legendStyle.Padding.Left, + Right: legend.Left + legendStyle.Padding.Left, + Bottom: legend.Top + legendStyle.Padding.Top, } legendStyle.GetTextOptions().WriteToRenderer(r) @@ -276,7 +395,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { if len(labels[x]) > 0 { tb := r.MeasureText(labels[x]) if labelCount > 0 { - legendContent.Bottom += DefaultMinimumTickVerticalSpacing + legendContent.Bottom += DefaultLegendVerticalSpacing } legendContent.Bottom += tb.Height() right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum @@ -286,8 +405,8 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { } legend = legend.Grow(legendContent) - legend.Right = legendContent.Right + legendPadding.Right - legend.Bottom = legendContent.Bottom + legendPadding.Bottom + legend.Right = legendContent.Right + legendStyle.Padding.Right + legend.Bottom = legendContent.Bottom + legendStyle.Padding.Bottom Draw.Box(r, legend, legendStyle) @@ -301,7 +420,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { label = labels[x] if len(label) > 0 { if legendCount > 0 { - ycursor += DefaultMinimumTickVerticalSpacing + ycursor += DefaultLegendVerticalSpacing } tb := r.MeasureText(label) @@ -313,7 +432,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { lx := tx + tb.Width() + lineTextGap ly := ty - th2 - lx2 := legendContent.Right - legendPadding.Right + lx2 := legendContent.Right - legendStyle.Padding.Right r.SetStrokeColor(lines[x].GetStrokeColor()) r.SetStrokeWidth(lines[x].GetStrokeWidth())