From 1ccfbb01725889599bf8290e05d3f4c1b4a20fdd Mon Sep 17 00:00:00 2001 From: twessling-icas Date: Mon, 22 May 2023 22:37:57 +0700 Subject: [PATCH] add logarithmic axes support, with tests. Supports positive Y-values only. (#141) Co-authored-by: Ton Wessling --- .gitignore | 3 +- examples/logarithmic_axes/main.go | 41 ++++++++++++ examples/logarithmic_axes/output.png | Bin 0 -> 16711 bytes logarithmic_range.go | 94 +++++++++++++++++++++++++++ logarithmic_range_test.go | 46 +++++++++++++ value_formatter.go | 5 ++ value_formatter_test.go | 7 ++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 examples/logarithmic_axes/main.go create mode 100644 examples/logarithmic_axes/output.png create mode 100644 logarithmic_range.go create mode 100644 logarithmic_range_test.go diff --git a/.gitignore b/.gitignore index 3e4b6e1..8f4388f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ # Other .vscode .DS_Store -coverage.html \ No newline at end of file +coverage.html +.idea diff --git a/examples/logarithmic_axes/main.go b/examples/logarithmic_axes/main.go new file mode 100644 index 0000000..9f329c8 --- /dev/null +++ b/examples/logarithmic_axes/main.go @@ -0,0 +1,41 @@ +package main + +//go:generate go run main.go + +import ( + "os" + + "github.com/wcharczuk/go-chart" +) + +func main() { + + /* + In this example we set the primary YAxis to have logarithmic range. + */ + + graph := chart.Chart{ + Background: chart.Style{ + Padding: chart.Box{ + Top: 20, + Left: 20, + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + Name: "A test series", + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1, 10, 100, 1000, 10000}, + }, + }, + YAxis: chart.YAxis{ + Style: chart.Shown(), + NameStyle: chart.Shown(), + Range: &chart.LogarithmicRange{}, + }, + } + + f, _ := os.Create("output.png") + defer f.Close() + graph.Render(chart.PNG, f) +} diff --git a/examples/logarithmic_axes/output.png b/examples/logarithmic_axes/output.png new file mode 100644 index 0000000000000000000000000000000000000000..4462b8d579482ace22d3f9f5ae92fa1c00aa4eb3 GIT binary patch literal 16711 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 z z94L*uRW{@E7m(-g{I`Zn;%paKSqZN2swW zDYcH<9B?mPEUdVHKBiuV)AQ1)039mh6peO`wD!TAs-0i=8NB2Z`^oeVOhFHnhYmTU zX7@hi%jv3h&Q#}2Yfm1$6k08Q%b}$2X+fFph5_yt+N<U!O_@km!lf|)7%76t+ vZo*(k%MO4ngK(cFCH$7>um4>*1*9zgQo4IUr0*UNv`JE2_Im17ZTJ5LBoZQ& literal 0 HcmV?d00001 diff --git a/logarithmic_range.go b/logarithmic_range.go new file mode 100644 index 0000000..5b183b3 --- /dev/null +++ b/logarithmic_range.go @@ -0,0 +1,94 @@ +package chart + +import ( + "fmt" + "math" +) + +// LogarithmicRange represents a boundary for a set of numbers. +type LogarithmicRange struct { + Min float64 + Max float64 + Domain int + Descending bool +} + +// IsDescending returns if the range is descending. +func (r LogarithmicRange) IsDescending() bool { + return r.Descending +} + +// IsZero returns if the LogarithmicRange has been set or not. +func (r LogarithmicRange) IsZero() bool { + return (r.Min == 0 || math.IsNaN(r.Min)) && + (r.Max == 0 || math.IsNaN(r.Max)) && + r.Domain == 0 +} + +// GetMin gets the min value for the continuous range. +func (r LogarithmicRange) GetMin() float64 { + return r.Min +} + +// SetMin sets the min value for the continuous range. +func (r *LogarithmicRange) SetMin(min float64) { + r.Min = min +} + +// GetMax returns the max value for the continuous range. +func (r LogarithmicRange) GetMax() float64 { + return r.Max +} + +// SetMax sets the max value for the continuous range. +func (r *LogarithmicRange) SetMax(max float64) { + r.Max = max +} + +// GetDelta returns the difference between the min and max value. +func (r LogarithmicRange) GetDelta() float64 { + return r.Max - r.Min +} + +// GetDomain returns the range domain. +func (r LogarithmicRange) GetDomain() int { + return r.Domain +} + +// SetDomain sets the range domain. +func (r *LogarithmicRange) SetDomain(domain int) { + r.Domain = domain +} + +// String returns a simple string for the LogarithmicRange. +func (r LogarithmicRange) String() string { + return fmt.Sprintf("LogarithmicRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) +} + +// Translate maps a given value into the LogarithmicRange space. Modified version from ContinuousRange. +func (r LogarithmicRange) Translate(value float64) int { + if value < 1 { + return 0 + } + normalized := math.Max(value-r.Min, 1) + ratio := math.Log10(normalized) / math.Log10(r.GetDelta()) + + if r.IsDescending() { + return r.Domain - int(math.Ceil(ratio*float64(r.Domain))) + } + + return int(math.Ceil(ratio * float64(r.Domain))) +} + +// GetTicks calculates the needed ticks for the axis, in log scale. Only supports Y values > 0. +func (r LogarithmicRange) GetTicks(render Renderer, defaults Style, vf ValueFormatter) []Tick { + var ticks []Tick + exponentStart := int64(math.Max(0, math.Floor(math.Log10(r.Min)))) // one below min + exponentEnd := int64(math.Max(0, math.Ceil(math.Log10(r.Max)))) // one above max + for exp:=exponentStart; exp<=exponentEnd; exp++ { + tickVal := math.Pow(10, float64(exp)) + ticks = append(ticks, Tick{Value: tickVal, Label: vf(tickVal)}) + } + + return ticks +} diff --git a/logarithmic_range_test.go b/logarithmic_range_test.go new file mode 100644 index 0000000..110f761 --- /dev/null +++ b/logarithmic_range_test.go @@ -0,0 +1,46 @@ +package chart + +import ( + "testing" + + "github.com/blend/go-sdk/assert" +) + +func TestLogRangeTranslate(t *testing.T) { + assert := assert.New(t) + values := []float64{1, 10, 100, 1000, 10000, 100000, 1000000} + r := LogarithmicRange{Domain: 1000} + r.Min, r.Max = MinMax(values...) + + assert.Equal(0, r.Translate(0)) // goes to bottom + assert.Equal(0, r.Translate(1)) // goes to bottom + assert.Equal(160, r.Translate(10)) // roughly 1/6th of max + assert.Equal(500, r.Translate(1000)) // roughly 1/2 of max (1.0e6 / 1.0e3) + assert.Equal(1000, r.Translate(1000000)) // max value +} + +func TestGetTicks(t *testing.T) { + assert := assert.New(t) + values := []float64{35, 512, 1525122} + r := LogarithmicRange{Domain: 1000} + r.Min, r.Max = MinMax(values...) + + ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) + assert.Equal(7, len(ticks)) + assert.Equal(10, ticks[0].Value) + assert.Equal(100, ticks[1].Value) + assert.Equal(10000000, ticks[6].Value) +} + +func TestGetTicksFromHigh(t *testing.T) { + assert := assert.New(t) + values := []float64{1412, 352144, 1525122} // min tick should be 1000 + r := LogarithmicRange{} + r.Min, r.Max = MinMax(values...) + + ticks := r.GetTicks(nil, Style{}, FloatValueFormatter) + assert.Equal(5, len(ticks)) + assert.Equal(float64(1000), ticks[0].Value) + assert.Equal(float64(10000), ticks[1].Value) + assert.Equal(float64(10000000), ticks[4].Value) +} diff --git a/value_formatter.go b/value_formatter.go index 468f3bd..1a2002a 100644 --- a/value_formatter.go +++ b/value_formatter.go @@ -103,3 +103,8 @@ func KValueFormatter(k float64, vf ValueFormatter) ValueFormatter { return fmt.Sprintf("%0.0fσ %s", k, vf(v)) } } + +// FloatValueFormatter is a ValueFormatter for float64, exponential notation, e.g. 1.52e+08. +func ExponentialValueFormatter(v interface{}) string { + return FloatValueFormatterWithFormat(v, "%.2e") +} diff --git a/value_formatter_test.go b/value_formatter_test.go index d9ffaef..808400f 100644 --- a/value_formatter_test.go +++ b/value_formatter_test.go @@ -56,3 +56,10 @@ func TestFloatValueFormatterWithFormat(t *testing.T) { testutil.AssertEqual(t, "123.456", sv) testutil.AssertEqual(t, "123.000", FloatValueFormatterWithFormat(123, "%.3f")) } + +func TestExponentialValueFormatter(t *testing.T) { + assert := assert.New(t) + assert.Equal("1.23e+02", ExponentialValueFormatter(123.456)) + assert.Equal("1.24e+07", ExponentialValueFormatter(12421243.424)) + assert.Equal("4.50e-01", ExponentialValueFormatter(0.45)) +}