add logarithmic axes support, with tests. Supports positive Y-values only. (#141)
Co-authored-by: Ton Wessling <twessling@ebay.com>
This commit is contained in:
parent
c1468e8ae4
commit
1ccfbb0172
7 changed files with 195 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -16,4 +16,5 @@
|
||||||
# Other
|
# Other
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
coverage.html
|
coverage.html
|
||||||
|
.idea
|
||||||
|
|
41
examples/logarithmic_axes/main.go
Normal file
41
examples/logarithmic_axes/main.go
Normal file
|
@ -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)
|
||||||
|
}
|
BIN
examples/logarithmic_axes/output.png
Normal file
BIN
examples/logarithmic_axes/output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
94
logarithmic_range.go
Normal file
94
logarithmic_range.go
Normal file
|
@ -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
|
||||||
|
}
|
46
logarithmic_range_test.go
Normal file
46
logarithmic_range_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -103,3 +103,8 @@ func KValueFormatter(k float64, vf ValueFormatter) ValueFormatter {
|
||||||
return fmt.Sprintf("%0.0fσ %s", k, vf(v))
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -56,3 +56,10 @@ func TestFloatValueFormatterWithFormat(t *testing.T) {
|
||||||
testutil.AssertEqual(t, "123.456", sv)
|
testutil.AssertEqual(t, "123.456", sv)
|
||||||
testutil.AssertEqual(t, "123.000", FloatValueFormatterWithFormat(123, "%.3f"))
|
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))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue