add tilt field to chart

This commit is contained in:
2026-07-17 16:14:10 -04:00
parent 93acf143f2
commit 35f0ea6db3
3 changed files with 42 additions and 13 deletions

View File

@@ -56,8 +56,15 @@ type ChartProps struct {
Horizontal bool // bar: categories down the y-axis, values along x
Smooth bool // line/area: Catmull-Rom spline
DonutRatio float64
// SegmentGap is the px gap/stroke between touching marks — pie/donut slices (a per-slice
// and full-pie stroke) and stacked/grouped bar segments. A pointer so an explicit 0 (no
// gap) is distinct from unset; nil defaults to 2.
SegmentGap *float64
ThreeD bool // extrude bars, tilt pie/donut (ignored on line/area)
Depth float64
// Tilt is the 3D pie/donut squash scalar: 0 → full tilt (edge-on), 1 → flat. A pointer so
// an explicit 0 is distinct from unset; nil defaults to 0.85.
Tilt *float64
Width float64 // internal viewBox width (default 640); the SVG scales to its container
Height float64 // default 300
@@ -89,6 +96,7 @@ const (
chartSegGap = 2.0
chartMarkR = 4.0
chartDefaultDepth = 16.0
chartDefaultTilt = 0.85 // 3D pie/donut vertical squash (0 → edge-on, 1 → flat)
)
var chartTokens = []string{
@@ -388,6 +396,15 @@ func chartDepth(p ChartProps) float64 {
}
return chartDefaultDepth
}
// chartSegGapPx is the surface gap between touching marks: the SegmentGap prop when set
// (including an explicit 0), else the 2px default.
func chartSegGapPx(p ChartProps) float64 {
if p.SegmentGap != nil {
return *p.SegmentGap
}
return chartSegGap
}
func chartDX(p ChartProps) float64 {
if chart3D(p) {
return chartDepth(p) * 0.7
@@ -639,7 +656,7 @@ func chartBars(p ChartProps, w, h float64) []barMark {
accNeg = to
}
isEnd := (v > 0 && s == lastPos) || (v < 0 && s == lastNeg)
inset := chartSegGap
inset := chartSegGapPx(p)
if isEnd {
inset = 0
}
@@ -672,8 +689,9 @@ func chartBars(p ChartProps, w, h float64) []barMark {
}
}
nS := maxi(1, len(vis))
groupSize := math.Min(bf*0.72, (chartBarMaxW+chartSegGap)*float64(nS))
each := math.Max(1, math.Min(chartBarMaxW, groupSize/float64(nS)-chartSegGap))
gap := chartSegGapPx(p)
groupSize := math.Min(bf*0.72, (chartBarMaxW+gap)*float64(nS))
each := math.Max(1, math.Min(chartBarMaxW, groupSize/float64(nS)-gap))
for i := 0; i < count; i++ {
g := catStart(p, w, h) + bf*float64(i) + (bf-groupSize)/2
for j, s := range vis {
@@ -859,10 +877,14 @@ func labelAt(p ChartProps, i int) string {
// ── radial body ─────────────────────────────────────────────────────────────────
func radialTilt(p ChartProps) float64 {
if p.ThreeD {
return 0.62
if !p.ThreeD {
return 1 // no tilt: a flat, face-on disc
}
return 1
t := chartDefaultTilt
if p.Tilt != nil {
t = *p.Tilt
}
return clampf(t, 0, 1) // vertical squash of the disc when tilted
}
type radialGeo struct{ cx, cy, rOut, rIn, k float64 }
@@ -1007,7 +1029,7 @@ func radialBody(p ChartProps, w, h float64, hv int) string {
if hv >= 0 && hv != s.idx {
op = 0.55
}
b.WriteString(`<path d="` + slicePathD(g.cx, g.cy, g.rOut, g.rIn, s.a0, s.a1, g.k) + `" fill="` + svgEsc(chartColor(p, s.idx)) + `" fill-opacity="` + num(op) + `" stroke="var(--color-surface)" stroke-width="` + num(chartSegGap) + `"/>`)
b.WriteString(`<path d="` + slicePathD(g.cx, g.cy, g.rOut, g.rIn, s.a0, s.a1, g.k) + `" fill="` + svgEsc(chartColor(p, s.idx)) + `" fill-opacity="` + num(op) + `" stroke="var(--color-surface)" stroke-width="` + num(chartSegGapPx(p)) + `"/>`)
}
}