fix(plot): keep extreme finite ranges drawable
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -49,6 +49,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
of drawing a NaN population and returning a NaN point with no
|
of drawing a NaN population and returning a NaN point with no
|
||||||
error.
|
error.
|
||||||
|
|
||||||
|
**Signal and plots.**
|
||||||
|
|
||||||
|
- `WriteSVG` keeps extreme but finite data and axis ranges drawable:
|
||||||
|
the padding, projection and tick arithmetic fall back to forms whose
|
||||||
|
terms stay in range, so the file never carries a NaN coordinate.
|
||||||
|
|
||||||
## [1.0.0] - 2026-09-03
|
## [1.0.0] - 2026-09-03
|
||||||
|
|
||||||
The initial release of Tensor, a scientific computing library in pure
|
The initial release of Tensor, a scientific computing library in pure
|
||||||
|
|||||||
+38
-6
@@ -119,11 +119,37 @@ func (c Chart) WriteSVG(path string) error {
|
|||||||
if !(yr[0] < yr[1]) || math.IsInf(yr[0], 0) || math.IsInf(yr[1], 0) {
|
if !(yr[0] < yr[1]) || math.IsInf(yr[0], 0) || math.IsInf(yr[1], 0) {
|
||||||
yr = bounds(all, false)
|
yr = bounds(all, false)
|
||||||
}
|
}
|
||||||
|
// project maps a value of the range [r0, r1] onto the plot span
|
||||||
|
// starting at plotLo. The normal form is the arithmetic the chart
|
||||||
|
// has always run; a span, or an offset from the range's start, that
|
||||||
|
// overflows float64 falls back to the halved form, whose every term
|
||||||
|
// a finite input keeps finite, so a range the data spans but the
|
||||||
|
// arithmetic cannot still draws instead of carrying a coordinate no
|
||||||
|
// renderer displays.
|
||||||
|
project := func(v, r0, r1, plotLo, plotSpan float64) float64 {
|
||||||
|
u := (v - r0) / (r1 - r0)
|
||||||
|
if math.IsNaN(u) || math.IsInf(u, 0) {
|
||||||
|
u = (v/2 - r0/2) / (r1/2 - r0/2)
|
||||||
|
}
|
||||||
|
return plotLo + u*plotSpan
|
||||||
|
}
|
||||||
px := func(x float64) float64 {
|
px := func(x float64) float64 {
|
||||||
return left + (x-xr[0])/(xr[1]-xr[0])*(float64(w)-left-right)
|
return project(x, xr[0], xr[1], left, float64(w)-left-right)
|
||||||
}
|
}
|
||||||
py := func(y float64) float64 {
|
py := func(y float64) float64 {
|
||||||
return float64(h) - bottom - (y-yr[0])/(yr[1]-yr[0])*(float64(h)-top-bottom)
|
return float64(h) - bottom - project(y, yr[0], yr[1], 0, float64(h)-top-bottom)
|
||||||
|
}
|
||||||
|
// tickValue places the k-th of the five ticks. A span that overflows
|
||||||
|
// float64 makes the affine form NaN or Inf, so the tick falls back to
|
||||||
|
// the convex combination, which stays between the range's own finite
|
||||||
|
// ends.
|
||||||
|
tickValue := func(r0, r1 float64, k int) float64 {
|
||||||
|
t := r0 + (r1-r0)*float64(k)/4
|
||||||
|
if math.IsNaN(t) || math.IsInf(t, 0) {
|
||||||
|
f := float64(k) / 4
|
||||||
|
t = r0*(1-f) + r1*f
|
||||||
|
}
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(xmlHeader)
|
b.WriteString(xmlHeader)
|
||||||
@@ -133,7 +159,7 @@ func (c Chart) WriteSVG(path string) error {
|
|||||||
left, esc(c.Title))
|
left, esc(c.Title))
|
||||||
// Axes with five ticks each.
|
// Axes with five ticks each.
|
||||||
for k := range 5 {
|
for k := range 5 {
|
||||||
t := xr[0] + (xr[1]-xr[0])*float64(k)/4
|
t := tickValue(xr[0], xr[1], k)
|
||||||
x := px(t)
|
x := px(t)
|
||||||
fmt.Fprintf(&b, "<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\" stroke=\"#ccc\" stroke-width=\"1\"/>\n",
|
fmt.Fprintf(&b, "<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\" stroke=\"#ccc\" stroke-width=\"1\"/>\n",
|
||||||
x, top, x, float64(h)-bottom)
|
x, top, x, float64(h)-bottom)
|
||||||
@@ -141,7 +167,7 @@ func (c Chart) WriteSVG(path string) error {
|
|||||||
x, float64(h)-bottom+16, tick(t))
|
x, float64(h)-bottom+16, tick(t))
|
||||||
}
|
}
|
||||||
for k := range 5 {
|
for k := range 5 {
|
||||||
t := yr[0] + (yr[1]-yr[0])*float64(k)/4
|
t := tickValue(yr[0], yr[1], k)
|
||||||
y := py(t)
|
y := py(t)
|
||||||
fmt.Fprintf(&b, "<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\" stroke=\"#ccc\" stroke-width=\"1\"/>\n",
|
fmt.Fprintf(&b, "<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\" stroke=\"#ccc\" stroke-width=\"1\"/>\n",
|
||||||
left, y, float64(w)-right, y)
|
left, y, float64(w)-right, y)
|
||||||
@@ -195,8 +221,14 @@ func bounds(pts []Point, xAxis bool) [2]float64 {
|
|||||||
if hi == lo {
|
if hi == lo {
|
||||||
hi = lo + 1
|
hi = lo + 1
|
||||||
}
|
}
|
||||||
pad := 0.05 * (hi - lo)
|
padLo, padHi := lo-0.05*(hi-lo), hi+0.05*(hi-lo)
|
||||||
return [2]float64{lo - pad, hi + pad}
|
if math.IsInf(padLo, 0) || math.IsInf(padHi, 0) {
|
||||||
|
// The padding, or the span it scales, overflows the range the
|
||||||
|
// finite data itself fits; the unpadded bounds keep every
|
||||||
|
// projection finite.
|
||||||
|
return [2]float64{lo, hi}
|
||||||
|
}
|
||||||
|
return [2]float64{padLo, padHi}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tick(v float64) string {
|
func tick(v float64) string {
|
||||||
|
|||||||
@@ -103,6 +103,39 @@ func TestWriteSVGErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWriteSVGExtremeFiniteValuesStayFinite pins the renderer against
|
||||||
|
// the extreme-but-finite corner: data, or an explicit axis range, whose
|
||||||
|
// span overflows float64 must not leak NaN or Inf coordinates into the
|
||||||
|
// drawing, because no renderer displays them and the non-finite
|
||||||
|
// refusal at the door guarantees every input point is finite.
|
||||||
|
func TestWriteSVGExtremeFiniteValuesStayFinite(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
padded := sampleChart()
|
||||||
|
padded.XRange = [2]float64{-1e308, 1e308}
|
||||||
|
padded.YRange = [2]float64{-1e308, 1e308}
|
||||||
|
charts := []struct {
|
||||||
|
name string
|
||||||
|
chart Chart
|
||||||
|
}{
|
||||||
|
{"wide-data.svg", Chart{Series: []Series{{Name: "wide",
|
||||||
|
Points: []Point{{X: 0, Y: -1e308}, {X: 1, Y: 1e308}}}}}},
|
||||||
|
{"wide-range.svg", padded},
|
||||||
|
}
|
||||||
|
for _, tc := range charts {
|
||||||
|
path := filepath.Join(dir, tc.name)
|
||||||
|
if err := tc.chart.WriteSVG(path); err != nil {
|
||||||
|
t.Fatalf("%s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
if s := string(body); strings.Contains(s, "NaN") || strings.Contains(s, "Inf") {
|
||||||
|
t.Fatalf("%s: the rendering leaked a non-finite coordinate: %s", tc.name, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustFromFloats(t *testing.T, values []float64, shape ...int) *core.Array {
|
func mustFromFloats(t *testing.T, values []float64, shape ...int) *core.Array {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
a, err := core.FromFloats(values, shape...)
|
a, err := core.FromFloats(values, shape...)
|
||||||
|
|||||||
Reference in New Issue
Block a user