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 0000000..4462b8d Binary files /dev/null and b/examples/logarithmic_axes/output.png differ 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)) +}