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
|
||||
.vscode
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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