add tilt field to chart
This commit is contained in:
@@ -54,3 +54,7 @@ export function numberToStringWithCommas(n: number):string {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clamp(val: number, min: number, max: number) : number {
|
||||||
|
return Math.min(Math.max(val, min), max);
|
||||||
|
}
|
||||||
@@ -48,10 +48,14 @@ export interface ChartProps {
|
|||||||
// donut only: inner-radius fraction of the outer radius (0.6 by default).
|
// donut only: inner-radius fraction of the outer radius (0.6 by default).
|
||||||
donutRatio?: number;
|
donutRatio?: number;
|
||||||
|
|
||||||
|
// Controls border/gap between pie/doughnut segments (acts as a per-slice & full pie "stroke")
|
||||||
|
segmentGap?: number; // value in px (default 2)
|
||||||
|
|
||||||
// Give the chart depth: bars extrude, pie/donut tilt and gain a rim (line/area ignore
|
// Give the chart depth: bars extrude, pie/donut tilt and gain a rim (line/area ignore
|
||||||
// it). An embellishment — flat reads more precisely — but sometimes wanted.
|
// it). An embellishment — flat reads more precisely — but sometimes wanted.
|
||||||
threeD?: boolean;
|
threeD?: boolean;
|
||||||
depth?: number; // 3D extrusion depth in px (default 16).
|
depth?: number; // 3D extrusion depth in px (default 16).
|
||||||
|
tilt?: number; // 3D tilt scalar (0 -> full tilt, 1 -> fully flattened)
|
||||||
|
|
||||||
height?: number; // px of the plot area (default 300). The legend adds its own height.
|
height?: number; // px of the plot area (default 300). The legend adds its own height.
|
||||||
width?: number; // fix the width instead of measuring the container.
|
width?: number; // fix the width instead of measuring the container.
|
||||||
@@ -83,7 +87,6 @@ const DEFAULT_HEIGHT = 300;
|
|||||||
const DEFAULT_WIDTH = 640; // used only until the container is measured (and on the server)
|
const DEFAULT_WIDTH = 640; // used only until the container is measured (and on the server)
|
||||||
const BAR_MAX_W = 24; // cap a bar's thickness; the band's leftover is deliberate air
|
const BAR_MAX_W = 24; // cap a bar's thickness; the band's leftover is deliberate air
|
||||||
const BAR_RADIUS = 4; // rounded data-end
|
const BAR_RADIUS = 4; // rounded data-end
|
||||||
const SEG_GAP = 2; // the surface gap between touching marks (stacked segments)
|
|
||||||
const MARK_R = 4; // hover marker radius (8px mark)
|
const MARK_R = 4; // hover marker radius (8px mark)
|
||||||
const DEFAULT_DEPTH = 16; // 3D extrusion depth
|
const DEFAULT_DEPTH = 16; // 3D extrusion depth
|
||||||
|
|
||||||
@@ -410,7 +413,7 @@ function CartesianChart(p: SubProps): JSXElement {
|
|||||||
const to = from + v;
|
const to = from + v;
|
||||||
if (v >= 0) accPos = to; else accNeg = to;
|
if (v >= 0) accPos = to; else accNeg = to;
|
||||||
const isEnd = (v > 0 && s === lastPos) || (v < 0 && s === lastNeg);
|
const isEnd = (v > 0 && s === lastPos) || (v < 0 && s === lastNeg);
|
||||||
const inset = isEnd ? 0 : SEG_GAP; // 2px surface gap between segments
|
const inset = isEnd ? 0 : (p.props.segmentGap ?? 2);
|
||||||
const vFrom = valuePos(from);
|
const vFrom = valuePos(from);
|
||||||
// pull the value end toward the baseline by the gap (except the outer end)
|
// pull the value end toward the baseline by the gap (except the outer end)
|
||||||
const vTo = valuePos(to) + (h ? (v >= 0 ? -inset : inset) : (v >= 0 ? inset : -inset));
|
const vTo = valuePos(to) + (h ? (v >= 0 ? -inset : inset) : (v >= 0 ? inset : -inset));
|
||||||
@@ -424,8 +427,8 @@ function CartesianChart(p: SubProps): JSXElement {
|
|||||||
// original series index.
|
// original series index.
|
||||||
const vis = series.map((_, s) => s).filter(shown);
|
const vis = series.map((_, s) => s).filter(shown);
|
||||||
const nS = Math.max(1, vis.length);
|
const nS = Math.max(1, vis.length);
|
||||||
const groupSize = Math.min(bf * 0.72, (BAR_MAX_W + SEG_GAP) * nS);
|
const groupSize = Math.min(bf * 0.72, (BAR_MAX_W + (p.props.segmentGap ?? 2)) * nS);
|
||||||
const each = Math.max(1, Math.min(BAR_MAX_W, groupSize / nS - SEG_GAP));
|
const each = Math.max(1, Math.min(BAR_MAX_W, groupSize / nS - (p.props.segmentGap ?? 2)));
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const g = catStart() + bf * i + (bf - groupSize) / 2;
|
const g = catStart() + bf * i + (bf - groupSize) / 2;
|
||||||
vis.forEach((s, j) => {
|
vis.forEach((s, j) => {
|
||||||
@@ -610,7 +613,7 @@ function RadialChart(p: SubProps): JSXElement {
|
|||||||
|
|
||||||
const threeD = () => !!p.props.threeD;
|
const threeD = () => !!p.props.threeD;
|
||||||
const depth = () => p.props.depth ?? DEFAULT_DEPTH;
|
const depth = () => p.props.depth ?? DEFAULT_DEPTH;
|
||||||
const tilt = () => (threeD() ? 0.62 : 1); // vertical squash of the disc when tilted
|
const tilt = () => (threeD() ? clamp((p.props.tilt ?? 0.85), 0, 1) : 1); // vertical squash of the disc when tilted
|
||||||
|
|
||||||
const values = () => p.props.series[0]?.data ?? [];
|
const values = () => p.props.series[0]?.data ?? [];
|
||||||
const labels = () => p.props.labels ?? values().map((_, i) => String(i + 1));
|
const labels = () => p.props.labels ?? values().map((_, i) => String(i + 1));
|
||||||
@@ -679,7 +682,7 @@ function RadialChart(p: SubProps): JSXElement {
|
|||||||
for (const s of slices()) {
|
for (const s of slices()) {
|
||||||
if (s.a1 <= s.a0) continue;
|
if (s.a1 <= s.a0) continue;
|
||||||
const op = hv === null || hv === s.idx ? 1 : 0.55;
|
const op = hv === null || hv === s.idx ? 1 : 0.55;
|
||||||
out.push(`<path d="${slicePathD(g.cx, g.cy, g.rOut, g.rIn, s.a0, s.a1, g.k)}" fill="${esc(p.colorOf(s.idx))}" fill-opacity="${op}" stroke="var(--color-surface)" stroke-width="${SEG_GAP}"/>`);
|
out.push(`<path d="${slicePathD(g.cx, g.cy, g.rOut, g.rIn, s.a0, s.a1, g.k)}" fill="${esc(p.colorOf(s.idx))}" fill-opacity="${op}" stroke="var(--color-surface)" stroke-width="${(p.props.segmentGap ?? 2)}"/>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out.join("");
|
return out.join("");
|
||||||
|
|||||||
@@ -56,8 +56,15 @@ type ChartProps struct {
|
|||||||
Horizontal bool // bar: categories down the y-axis, values along x
|
Horizontal bool // bar: categories down the y-axis, values along x
|
||||||
Smooth bool // line/area: Catmull-Rom spline
|
Smooth bool // line/area: Catmull-Rom spline
|
||||||
DonutRatio float64
|
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)
|
ThreeD bool // extrude bars, tilt pie/donut (ignored on line/area)
|
||||||
Depth float64
|
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
|
Width float64 // internal viewBox width (default 640); the SVG scales to its container
|
||||||
Height float64 // default 300
|
Height float64 // default 300
|
||||||
@@ -89,6 +96,7 @@ const (
|
|||||||
chartSegGap = 2.0
|
chartSegGap = 2.0
|
||||||
chartMarkR = 4.0
|
chartMarkR = 4.0
|
||||||
chartDefaultDepth = 16.0
|
chartDefaultDepth = 16.0
|
||||||
|
chartDefaultTilt = 0.85 // 3D pie/donut vertical squash (0 → edge-on, 1 → flat)
|
||||||
)
|
)
|
||||||
|
|
||||||
var chartTokens = []string{
|
var chartTokens = []string{
|
||||||
@@ -388,6 +396,15 @@ func chartDepth(p ChartProps) float64 {
|
|||||||
}
|
}
|
||||||
return chartDefaultDepth
|
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 {
|
func chartDX(p ChartProps) float64 {
|
||||||
if chart3D(p) {
|
if chart3D(p) {
|
||||||
return chartDepth(p) * 0.7
|
return chartDepth(p) * 0.7
|
||||||
@@ -639,7 +656,7 @@ func chartBars(p ChartProps, w, h float64) []barMark {
|
|||||||
accNeg = to
|
accNeg = to
|
||||||
}
|
}
|
||||||
isEnd := (v > 0 && s == lastPos) || (v < 0 && s == lastNeg)
|
isEnd := (v > 0 && s == lastPos) || (v < 0 && s == lastNeg)
|
||||||
inset := chartSegGap
|
inset := chartSegGapPx(p)
|
||||||
if isEnd {
|
if isEnd {
|
||||||
inset = 0
|
inset = 0
|
||||||
}
|
}
|
||||||
@@ -672,8 +689,9 @@ func chartBars(p ChartProps, w, h float64) []barMark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
nS := maxi(1, len(vis))
|
nS := maxi(1, len(vis))
|
||||||
groupSize := math.Min(bf*0.72, (chartBarMaxW+chartSegGap)*float64(nS))
|
gap := chartSegGapPx(p)
|
||||||
each := math.Max(1, math.Min(chartBarMaxW, groupSize/float64(nS)-chartSegGap))
|
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++ {
|
for i := 0; i < count; i++ {
|
||||||
g := catStart(p, w, h) + bf*float64(i) + (bf-groupSize)/2
|
g := catStart(p, w, h) + bf*float64(i) + (bf-groupSize)/2
|
||||||
for j, s := range vis {
|
for j, s := range vis {
|
||||||
@@ -859,10 +877,14 @@ func labelAt(p ChartProps, i int) string {
|
|||||||
// ── radial body ─────────────────────────────────────────────────────────────────
|
// ── radial body ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func radialTilt(p ChartProps) float64 {
|
func radialTilt(p ChartProps) float64 {
|
||||||
if p.ThreeD {
|
if !p.ThreeD {
|
||||||
return 0.62
|
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 }
|
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 {
|
if hv >= 0 && hv != s.idx {
|
||||||
op = 0.55
|
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)) + `"/>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user