Skip to content

Commit

Permalink
Improved range calculation and NaN y-axis fix
Browse files Browse the repository at this point in the history
This fixes #7 by creating specific range calculation logic for when the max and min value are the same (there is no span).
In addition the Y-Axis label count is now more intelligently calculated.
  • Loading branch information
jentfoo committed Feb 14, 2024
1 parent 575c859 commit 03160c2
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 6 deletions.
17 changes: 14 additions & 3 deletions charts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const labelFontSize = 10
const smallLabelFontSize = 8
const defaultDotWidth = 2.0
const defaultStrokeWidth = 2.0
const defaultYAxisLabelCount = 10
const defaultYAxisLabelCountHigh = 10
const defaultYAxisLabelCountLow = 3

var defaultChartWidth = 600
var defaultChartHeight = 400
Expand Down Expand Up @@ -43,6 +44,15 @@ func GetNullValue() float64 {
return nullValue
}

func defaultYAxisLabelCount(span float64, decimalData bool) int {
result := math.Min(math.Max(span+1, defaultYAxisLabelCountLow), defaultYAxisLabelCountHigh)
if decimalData {
// if there is a decimal, we double our labels to provide more detailed labels
result = math.Min(result*2, defaultYAxisLabelCountHigh)
}
return int(result)
}

type Renderer interface {
Render() (Box, error)
}
Expand Down Expand Up @@ -169,6 +179,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
maxPadRange = *yAxisOption.RangeValuePaddingScale
}
min, max := opt.SeriesList.GetMinMax(index)
decimalData := (max - min) != math.Floor(max-min)
if yAxisOption.Min != nil && *yAxisOption.Min < min {
min = *yAxisOption.Min
minPadRange = 0.0
Expand All @@ -195,7 +206,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
if yAxisOption.Unit > 0 {
padLabelCount = int((max-min)/yAxisOption.Unit) + 1
} else {
padLabelCount = defaultYAxisLabelCount
padLabelCount = defaultYAxisLabelCount(max-min, decimalData)
}
}
// we call padRange directly because we need to do this padding before we can calculate the final labelCount for the axisRange
Expand All @@ -207,7 +218,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
}
labelCount = int((max-min)/yAxisOption.Unit) + 1
} else {
labelCount = defaultYAxisLabelCount
labelCount = defaultYAxisLabelCount(max-min, decimalData)
}
yAxisOption.LabelCount = labelCount
}
Expand Down
28 changes: 28 additions & 0 deletions line_chart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,34 @@ func TestLineChart(t *testing.T) {
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><text x=\"10\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1.5k</text><text x=\"23\" y=\"133\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1k</text><text x=\"13\" y=\"250\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">500</text><text x=\"31\" y=\"367\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">0</text><path d=\"M 88 332\nL 165 330\nL 242 337\nL 319 329\nL 396 339\nL 473 307\nL 551 311\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><path d=\"M 88 169\nL 165 143\nL 242 150\nL 319 143\nL 396 59\nL 473 50\nL 551 52\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/></svg>",
},
{
name: "ZeroData",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
values := [][]float64{
{0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0},
}
opt.SeriesList = NewSeriesListDataFromValues(values)
return opt
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><text x=\"10\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">2</text><text x=\"10\" y=\"192\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1</text><text x=\"10\" y=\"367\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">0</text><path d=\"M 29 10\nL 590 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 29 185\nL 590 185\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 69 360\nL 149 360\nL 229 360\nL 309 360\nL 389 360\nL 469 360\nL 549 360\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><path d=\"M 69 360\nL 149 360\nL 229 360\nL 309 360\nL 389 360\nL 469 360\nL 549 360\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/></svg>",
},
{
name: "TinyRange",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
values := [][]float64{
{0.1, 0.2, 0.1, 0.2, 0.4, 0.2, 0.1},
{0.2, 0.4, 0.8, 0.4, 0.2, 0.1, 0.2},
}
opt.SeriesList = NewSeriesListDataFromValues(values)
return opt
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<path d=\"M 0 0\nL 600 0\nL 600 400\nL 0 400\nL 0 0\" style=\"stroke-width:0;stroke:none;fill:rgba(255,255,255,1.0)\"/><text x=\"32\" y=\"17\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">2</text><text x=\"19\" y=\"87\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1.6</text><text x=\"10\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">1.20</text><text x=\"19\" y=\"227\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">0.8</text><text x=\"19\" y=\"297\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">0.4</text><text x=\"32\" y=\"367\" style=\"stroke-width:0;stroke:none;fill:rgba(70,70,70,1.0);font-size:15.3px;font-family:'Roboto Medium',sans-serif\">0</text><path d=\"M 51 10\nL 590 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 51 80\nL 590 80\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 51 150\nL 590 150\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 51 220\nL 590 220\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 51 290\nL 590 290\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 89 343\nL 166 325\nL 243 343\nL 320 325\nL 397 290\nL 474 325\nL 551 343\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><path d=\"M 89 325\nL 166 290\nL 243 220\nL 320 290\nL 397 325\nL 474 343\nL 551 325\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:none\"/></svg>",
},
}

for _, tt := range tests {
Expand Down
4 changes: 2 additions & 2 deletions mark_line_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ func TestMarkLine(t *testing.T) {
FontColor: drawing.ColorBlack,
StrokeColor: drawing.ColorBlack,
Series: series,
Range: NewRange(p, p.Height(), 6, 0.0, 5.0, 1.0, 1.0),
Range: NewRange(p, p.Height(), 6, 0.0, 5.0, 0.0, 0.0),
})
if _, err := markLine.Render(); err != nil {
return nil, err
}
return p.Bytes()
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<circle cx=\"23\" cy=\"175\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 175\nL 562 175\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 170\nL 578 175\nL 562 180\nL 567 175\nL 562 170\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"179\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text><circle cx=\"23\" cy=\"243\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 243\nL 562 243\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 238\nL 578 243\nL 562 248\nL 567 243\nL 562 238\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"247\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><circle cx=\"23\" cy=\"312\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 312\nL 562 312\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 307\nL 578 312\nL 562 317\nL 567 312\nL 562 307\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"316\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"600\" height=\"400\">\\n<circle cx=\"23\" cy=\"164\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 164\nL 562 164\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 159\nL 578 164\nL 562 169\nL 567 164\nL 562 159\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"168\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3</text><circle cx=\"23\" cy=\"236\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 236\nL 562 236\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 231\nL 578 236\nL 562 241\nL 567 236\nL 562 231\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"240\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">2</text><circle cx=\"23\" cy=\"308\" r=\"3\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 29 308\nL 562 308\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 562 303\nL 578 308\nL 562 313\nL 567 308\nL 562 303\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,0,1.0)\"/><text x=\"580\" y=\"312\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">1</text></svg>",
},
}
for i, tt := range tests {
Expand Down
12 changes: 11 additions & 1 deletion range.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const rangeMinPaddingPercentMin = 0.0 // increasing could result in forced negat
const rangeMinPaddingPercentMax = 20.0
const rangeMaxPaddingPercentMin = 5.0 // set minimum spacing at the top of the graph
const rangeMaxPaddingPercentMax = 20.0
const zeroSpanAdjustment = 1 // Adjustment

type axisRange struct {
p *Painter
Expand Down Expand Up @@ -77,8 +78,17 @@ rootLoop:
minResult = minTrunk // remove possible float multiplication inaccuracies
}

if maxPaddingScale <= 0.0 {
if max == minResult {
// no adjustment was made and there is no span, because of that the max calculation below can't function
// for that reason we apply a default constant span, still wanting to prefer a zero start if possible
if minResult == 0 {
return minResult, minResult + (2 * zeroSpanAdjustment)
}
return minResult - zeroSpanAdjustment, minResult + zeroSpanAdjustment
} else if maxPaddingScale <= 0.0 {
return minResult, max
} else if math.Abs(max) < 10 {
return minResult, math.Ceil(max) + 1
}

// update max to provide ideal padding and human friendly intervals
Expand Down

0 comments on commit 03160c2

Please sign in to comment.