@graphysdk/geom-waterfall 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,17 @@
1
+ # License
2
+
3
+ This software is available under any of the following licenses, at your option,
4
+ depending on which one's conditions you meet:
5
+
6
+ - **PolyForm Noncommercial 1.0.0** — for noncommercial use (personal,
7
+ charitable, educational, research, government).
8
+ See [licenses/PolyForm-NonCommercial-1.0.0.md](licenses/PolyForm-NonCommercial-1.0.0.md)
9
+
10
+ - **PolyForm Small Business 1.0.0** — free commercial use for organizations
11
+ under the size thresholds defined in the license.
12
+ See [licenses/PolyForm-Small-Business-1.0.0.md](licenses/PolyForm-Small-Business-1.0.0.md)
13
+
14
+ - **PolyForm Free Trial 1.0.0** — a 32-day evaluation for anyone else.
15
+ See [licenses/PolyForm-Free-Trial-1.0.0.md](licenses/PolyForm-Free-Trial-1.0.0.md)
16
+
17
+ For commercial use beyond these terms, contact [hello@graphy.dev](mailto:hello@graphy.dev) or visit [https://graphy.dev](https://graphy.dev).
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # @graphysdk/geom-waterfall
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Beta.** This geom is on a `0.x` version line: a minor release may change its aesthetics, style parts or look. Use a caret range (`^0.1.0`) to take only patches.
4
+
5
+ A waterfall geom for the Graphy charting SDK. It shows how a running total gets from where it starts to where it ends: one floating bar per change, coloured by whether it adds or takes away, with totals standing on zero and a connector carrying the total from each bar to the next.
6
+
7
+ Learn more at [graphy.dev](https://graphy.dev).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @graphysdk/geom-waterfall @graphysdk/react
13
+ ```
14
+
15
+ Requires React 19. Its only peer is `@graphysdk/react`.
16
+
17
+ ## Usage
18
+
19
+ Data is one row per bar, in the order the total runs. Map the category to `x` and the change to `y`. Map `measure` to a column that marks a row `'total'` (draw the running total so far) or `'absolute'` (set the running total to the row's value); an empty or `'relative'` row is a step, and any other value is an error:
20
+
21
+ ```tsx
22
+ import { createGraphyKit, GraphRenderer } from '@graphysdk/react';
23
+ import { waterfall } from '@graphysdk/geom-waterfall';
24
+
25
+ const kit = createGraphyKit({ plugins: [waterfall] });
26
+
27
+ const data = {
28
+ columns: [{ key: 'item' }, { key: 'amount' }, { key: 'measure' }],
29
+ rows: [
30
+ { item: 'Opening', amount: 100, measure: 'absolute' },
31
+ { item: 'Sales', amount: 60 },
32
+ { item: 'Payroll', amount: -50 },
33
+ { item: 'Closing', measure: 'total' },
34
+ ],
35
+ };
36
+
37
+ const spec = kit.pipe(
38
+ kit.createSpec({ x: 'item', y: 'amount' }),
39
+ kit.geom.waterfall({ aes: { measure: 'measure' } }),
40
+ kit.scale.x.discrete(),
41
+ kit.scale.y.continuous()
42
+ );
43
+
44
+ export function CashFlow() {
45
+ return (
46
+ <kit.GraphProvider spec={spec} data={data}>
47
+ <GraphRenderer />
48
+ </kit.GraphProvider>
49
+ );
50
+ }
51
+ ```
52
+
53
+ When the running totals are already in the data, set `stat: kit.stat.identity()` and map `start` and `end` instead of `y`; `measure` still marks the totals. To colour the kinds through a scale with a legend, map `color: 'waterfallKind'`.
54
+
55
+ `params.width` sets each bar's share of its band, in `(0, 1]` (default `0.8`), and `params.connector` sets where the line carrying the total between bars runs: `'spanning'` (default) or `'between'`.
56
+
57
+ Colour the bars with `kit.style.geom.waterfall.increase()`, `.decrease()` and `.total()`, the lines between them with `kit.style.geom.waterfall.connector()` (`alpha: 0` hides them, and a `where` picks the ones leaving some bars), and the bars' shared look with `kit.style.geom.waterfall()`.
58
+
59
+ ## Docs
60
+
61
+ - [Waterfall](https://docs.graphy.dev/sdk-next/graph-types/waterfall)
62
+
63
+ ## Feedback
64
+
65
+ This geom is released early to hear what works. Tell us on [Discord](https://discord.gg/yGmYCjkSr).
66
+
67
+ ## Licence
68
+
69
+ This package is available under PolyForm Noncommercial, Small Business, or a 32-day Free Trial. Free Trial is local or internal evaluation only — it does not cover a public page. See [what you can do under the trial](https://docs.graphy.dev/licence/evaluation).
70
+
71
+ For a commercial licence, [talk to us](https://graphy.dev/license).
@@ -0,0 +1,28 @@
1
+ import { WaterfallKind } from '../../utils/waterfall-columns';
2
+ /**
3
+ * How a row's value reads, as Plotly's waterfall names it: a `'relative'` step changes the running total by
4
+ * the value, a `'total'` draws the running total so far and ignores its value, and an `'absolute'` bar sets
5
+ * the running total to its value.
6
+ */
7
+ export type WaterfallMeasure = 'relative' | 'total' | 'absolute';
8
+ /** One bar, in data units: the running total it starts from and the one it ends on. */
9
+ export interface WaterfallStep {
10
+ start: number;
11
+ end: number;
12
+ }
13
+ /**
14
+ * Reads each row's measure. A value that is neither empty nor a known measure throws, so a misspelt total
15
+ * (`'Total'`) is reported rather than drawn as a step.
16
+ */
17
+ export declare const readMeasures: (values: readonly unknown[], variableName: string) => WaterfallMeasure[];
18
+ /**
19
+ * Runs the total through the rows in order and returns each row's bar. A step whose value is missing draws
20
+ * nothing (`null`) and leaves the total where it was; a total needs no value of its own. `measures` may be
21
+ * shorter than `values` (no measure mapped): a row without one is a step.
22
+ */
23
+ export declare const accumulateSteps: (values: ReadonlyArray<number | null>, measures: readonly WaterfallMeasure[]) => Array<WaterfallStep | null>;
24
+ /**
25
+ * What a bar is: a total or an absolute bar is a `'total'`, and a step goes up or down with its interval. A
26
+ * step of no change is an increase.
27
+ */
28
+ export declare const classifyStep: (step: WaterfallStep, measure: WaterfallMeasure) => WaterfallKind;
@@ -0,0 +1,9 @@
1
+ import { PlacementResult, PluginPlacementContext } from '@graphysdk/react';
2
+ /**
3
+ * A label per bar, printing the figure it shows: its change for a step, the running total for a total. The bar
4
+ * runs from the total before it to the total after, so `justify: 'end'` is the end it lands on, as a bar's is
5
+ * its tip: an increase labels above, a decrease below. Under `'auto'` each label sits just past that end, clear
6
+ * of the connector carrying the total on. Stack totals and category labels are bar vocabulary; a waterfall
7
+ * places neither.
8
+ */
9
+ export declare const placeWaterfallDataLabels: (context: PluginPlacementContext) => PlacementResult;
@@ -0,0 +1,132 @@
1
+ import { AnchorContext, AnchorPosition, GeomCompileResult, GeomCompilerInput, GeomMappingValidationInput, GeomObservationPart, GeomStylePart, GeomStyleTarget, Observation, PlacementResult, PluginPlacementContext, UserInputIssue, Geom } from '@graphysdk/react';
2
+ import { WaterfallGeomParams } from '../utils/waterfall-params';
3
+ import { WaterfallStat } from './waterfall.stat';
4
+ /** The bar's own paint, under the colour its kind gives it. */
5
+ declare const BAR_VOCABULARY: {
6
+ readonly alpha: "unitInterval";
7
+ readonly fillAlpha: "unitInterval";
8
+ readonly strokeWidth: "pixels";
9
+ };
10
+ declare const KIND_VOCABULARY: {
11
+ readonly fill: "color";
12
+ readonly stroke: "color";
13
+ };
14
+ declare const CONNECTOR_VOCABULARY: {
15
+ readonly stroke: "color";
16
+ readonly alpha: "unitInterval";
17
+ readonly strokeWidth: "pixels";
18
+ readonly dashArray: "dashArray";
19
+ };
20
+ /** Named so the published types reach the observations and every part through `@graphysdk/react`, the package's one peer. */
21
+ interface WaterfallStyleTarget extends GeomStyleTarget {
22
+ observation: GeomObservationPart<typeof BAR_VOCABULARY> & {
23
+ sharedPaint: false;
24
+ };
25
+ parts: {
26
+ increase: GeomStylePart<typeof KIND_VOCABULARY>;
27
+ decrease: GeomStylePart<typeof KIND_VOCABULARY>;
28
+ total: GeomStylePart<typeof KIND_VOCABULARY>;
29
+ connector: GeomStylePart<typeof CONNECTOR_VOCABULARY>;
30
+ };
31
+ }
32
+ /**
33
+ * A waterfall: how a running total gets from where it starts to where it ends, one floating bar per change,
34
+ * coloured by whether it adds or takes away, with a line carrying the total on from each bar to the next.
35
+ *
36
+ * By default data is one row per bar, in the order the total runs: `x` is the category and `y` the change,
37
+ * and the `waterfall` stat runs the total. An optional `measure` column marks a row `'total'` (the bar draws
38
+ * the running total so far) or `'absolute'` (the bar sets the running total to its value); an empty or
39
+ * `'relative'` row is a step. Under `stat: 'identity'` the totals are already run: the author maps `start` and
40
+ * `end`, and `measure` still marks the totals. A horizontal waterfall is `coord.flip()`.
41
+ */
42
+ export declare class WaterfallGeom extends Geom<WaterfallGeomParams> {
43
+ readonly type: "waterfall";
44
+ readonly defaultParams: WaterfallGeomParams;
45
+ readonly defaultStat: WaterfallStat;
46
+ readonly derivedVariables: ("waterfallStart" | "waterfallEnd" | "waterfallKind" | "waterfallValue")[];
47
+ /**
48
+ * The bar is the `y` interval between its `start` and `end`, lower first, so it drives the value axis; both
49
+ * ends are scaled on the same axis, the end for the connector.
50
+ */
51
+ readonly positionRoles: readonly [{
52
+ readonly axis: "x";
53
+ readonly role: "point";
54
+ readonly valueKind: "value";
55
+ }, {
56
+ readonly axis: "y";
57
+ readonly role: "min";
58
+ readonly valueKind: "value";
59
+ }, {
60
+ readonly axis: "y";
61
+ readonly role: "max";
62
+ readonly valueKind: "value";
63
+ }, {
64
+ readonly axis: "y";
65
+ readonly role: "scalar";
66
+ readonly valueKind: "value";
67
+ readonly aes: "start";
68
+ }, {
69
+ readonly axis: "y";
70
+ readonly role: "scalar";
71
+ readonly valueKind: "value";
72
+ readonly aes: "end";
73
+ }];
74
+ /**
75
+ * `y` is the change the stat runs the total through; `measure` marks the totals under any stat. `color` is
76
+ * opt-in: unmapped, the kind parts give each bar its colour and no legend is drawn.
77
+ */
78
+ readonly aesthetics: readonly [{
79
+ readonly kind: "data";
80
+ readonly name: "y";
81
+ }, {
82
+ readonly kind: "data";
83
+ readonly name: "measure";
84
+ }, {
85
+ readonly kind: "visual";
86
+ readonly name: "color";
87
+ }];
88
+ /** One running total, so there is nothing to dodge, and a stacked step would start on the wrong total. */
89
+ readonly supportedPositions: readonly ["identity"];
90
+ readonly supportedCoordTypes: readonly ["cartesian", "flip"];
91
+ /** A bar is a length along the value axis, so the axis holds zero, as a bar chart's does. */
92
+ readonly scaleConstraints: {
93
+ discreteMainAxis: boolean;
94
+ zeroBaseline: boolean;
95
+ };
96
+ readonly dataLabelCoordTypes: readonly ["cartesian", "flip"];
97
+ readonly getDataLabelPlacements: (context: PluginPlacementContext) => PlacementResult;
98
+ /**
99
+ * A hovered category is one unit: its bar is a rectangle, so a press anywhere across it is on the category, and
100
+ * the whole of its band takes a press however short its bar.
101
+ */
102
+ readonly spatialKind = "rects";
103
+ readonly highlightStrategy: "observation-rerender";
104
+ /** The figure the bar shows (its change, or the total it draws), then the running total it lands on. */
105
+ readonly tooltip: {
106
+ key: string;
107
+ aes: string;
108
+ }[];
109
+ readonly styleTarget: WaterfallStyleTarget;
110
+ /** A bar spans `width` of its band, so an axis annotation aligned to an edge lands where the bars end. */
111
+ readonly resolveBandFraction: Geom['resolveBandFraction'];
112
+ /**
113
+ * Substitutes an undrawable param and reports it, so the same spec can't diverge across renderers. A `width`
114
+ * above `1` would overlap the neighbouring bars, so it clamps to the full band; at or below `0` nothing is
115
+ * drawable, so the default takes over. An unknown `connector` falls back to `'spanning'`.
116
+ */
117
+ resolveParams(options: Parameters<Geom['resolveParams']>[0]): Record<string, unknown>;
118
+ /** A pinned number or a difference arrow on a bar measures the figure it shows, whichever stat ran. */
119
+ readonly resolveValueSource: Geom['resolveValueSource'];
120
+ /**
121
+ * Under the `waterfall` stat the bar's ends come from the changes in `y`, so a mapped end would be
122
+ * overwritten. Under any other stat the author maps both ends.
123
+ */
124
+ readonly validateMapping: ({ mapping, computedVariables, }: GeomMappingValidationInput) => readonly UserInputIssue[];
125
+ /**
126
+ * Where the editor raises its annotation trigger and where an annotation on a bar lands: on the running
127
+ * total the bar ends on, at the band centre, which is the figure a waterfall is read for.
128
+ */
129
+ readonly resolveAnchorPosition: (observation: Observation, { coordSystem }: AnchorContext) => AnchorPosition | null;
130
+ compile({ data, mapping, params }: GeomCompilerInput): GeomCompileResult;
131
+ }
132
+ export {};
@@ -0,0 +1,14 @@
1
+ import { StatCompileResult, StatCompilerInput, Stat } from '@graphysdk/react';
2
+ /** Every aesthetic the running total provides, so a layer under this stat maps none of them itself. */
3
+ export declare const WATERFALL_STAT_AESTHETICS: ReadonlySet<string>;
4
+ /**
5
+ * Runs the total through the changes in `y`, in data order, and writes where each bar starts and ends and what
6
+ * kind of bar it is, so the geom's compile need not read the measures a second time. The
7
+ * optional `measure` marks a total or an absolute bar. Only an explicit `group` splits the total: a discrete
8
+ * `color` usually paints the bars (by `measure`, say) and must not break one bridge into several.
9
+ */
10
+ export declare class WaterfallStat extends Stat {
11
+ readonly type: "waterfall";
12
+ readonly computedVariables: ReadonlySet<string>;
13
+ protected computeStat({ data, mapping }: StatCompilerInput): StatCompileResult;
14
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let s=require("@graphysdk/react"),f=require("react"),u=require("react/jsx-runtime");var h={start:"waterfallStart",end:"waterfallEnd",kind:"waterfallKind",value:"waterfallValue"},I=(e,t)=>{const r=t==="x"?(0,s.getX)(e):(0,s.getY)(e),a=t==="x"?(0,s.getXMin)(e):(0,s.getYMin)(e),o=t==="x"?(0,s.getXMax)(e):(0,s.getYMax)(e),n=t==="x"?(0,s.getYMin)(e):(0,s.getXMin)(e),d=t==="x"?(0,s.getYMax)(e):(0,s.getXMax)(e),i=(0,s.getScaledAesthetic)(e,"start"),l=(0,s.getScaledAesthetic)(e,"end"),c=W(e[h.kind]);return r===null||a===null||o===null||n===null||d===null||i===null||l===null||c===null?null:{observation:e,band:r,bandStart:a,bandEnd:o,low:Math.min(n,d),high:Math.max(n,d),start:i,end:l,kind:c}},y=(e,t)=>{const r=[];for(const a of e){const o=I(a,t);o!==null&&r.push(o)}return r},T=e=>e===void 0?null:typeof e=="string"?e:"variable"in e?e.variable:null,W=e=>e==="increase"||e==="decrease"||e==="total"?e:null,B=["between","spanning"],R=.8,V=({width:e})=>e??R,$=({connector:e})=>C(e)?e:"spanning",C=e=>B.some(t=>t===e),j=new Set(["relative","total","absolute"]),G=e=>e==null||e==="",_=(e,t)=>e.map(r=>{if(G(r))return"relative";if(j.has(r))return r;throw new s.UserInputError({code:"INVALID_DATA_SHAPE",message:`waterfall measure must be 'relative', 'total' or 'absolute' (got '${String(r)}' in variable '${t}')`,context:{aesthetic:"measure",variableName:t},suggestion:"Mark each total 'total' and each opening balance 'absolute'; leave a step empty or 'relative'."})}),D=(e,t)=>{let r=0;return e.map((a,o)=>{const n=t[o]??"relative";if(n==="total")return{start:0,end:r};if(a===null||!Number.isFinite(a))return null;if(n==="absolute")return r=a,{start:0,end:a};const d=r;return r+=a,{start:d,end:r}})},w=(e,t)=>t!=="relative"?"total":e.end<e.start?"decrease":"increase",L={start:h.start,end:h.end},M=new Set(Object.keys(L)),K=class extends s.Stat{constructor(...e){super(...e),this.type="waterfall",this.computedVariables=M}computeStat({data:e,mapping:t}){const r=b(t.y);if(r===null||!e.hasVariable(r))return{data:e,mapping:{}};if(e.size()>0&&e.getType(r)!=="numeric"){const i=e.getType(r);throw new s.UserInputError({code:"INCOMPATIBLE_TYPE",message:`'waterfall' stat requires a numeric 'y' (got '${i}' for variable '${r}')`,context:{aesthetic:"y",variableName:r,variableType:i},suggestion:"Map each bar's change to 'y', or running totals to 'start' and 'end' under stat 'identity'."})}const a=b(t.measure),o=a!==null&&e.hasVariable(a)?_(e.getValues(a),a):[],n=b(t.group),d=n!==null&&e.hasVariable(n)?[n]:[];return{data:F(e,r,o,Y(e,d)),mapping:L}}},F=(e,t,r,a)=>{const o=e.getValues(t,{type:"numeric"}),n=H(o,r,a),d=e.getValueFormat(t);return e.addVariable(h.start,"numeric",n.map(i=>i?.start??null),d).addVariable(h.end,"numeric",n.map(i=>i?.end??null),d).addVariable(h.kind,"categorical",n.map((i,l)=>i===null?null:w(i,r[l]??"relative")))},H=(e,t,r)=>{const a=new Map;r.forEach((n,d)=>{const i=a.get(n);i===void 0?a.set(n,[d]):i.push(d)});const o=e.map(()=>null);for(const n of a.values()){const d=D(n.map(i=>e[i]??null),n.map(i=>t[i]??"relative"));n.forEach((i,l)=>{o[i]=d[l]??null})}return o},Y=(e,t)=>{const r=t.map(a=>e.getValues(a));return Array.from({length:e.size()},(a,o)=>JSON.stringify(r.map(n=>n[o])))},b=e=>e===void 0?null:typeof e=="string"?e:"variable"in e?e.variable:null,X=e=>{const t=[];if(!e.layer.dataLabels.showDataLabels)return{labels:t,droppedCount:0};const{position:r,justify:a,align:o,offset:n}=e.layer.dataLabels,d=r==="auto"?"outside":r,i=(0,s.readMainAxis)(e.coordSystem),l=(0,s.buildPanelAnchorBox)(e.panelRect);for(const c of y(e.layer.data,i)){const g=e.formatLabel(c.observation);if(g==="")continue;const{width:p,height:v}=e.measureDataLabel("observation",d,g),m=(0,s.resolveAnchoredLabelCenter)({box:U(c,i,e.panelRect),panel:l,orientation:q(c,i),placement:d,justify:r==="auto"?"end":a,align:o,offset:n,labelWidth:p,labelHeight:v});t.push({layerId:e.layer.id,x:m.x,y:m.y,text:g,width:p,height:v,isRotated:!1,role:"observation",position:m.position,backdropColor:m.position==="inside"?(0,s.readBackdropColor)(e.styleReaders,c.observation,e.styleReaders.part(c.kind)?.get("fill",c.observation)):void 0})}return{labels:t,droppedCount:0}},U=(e,t,r)=>{const a=(0,s.composePoint)(t,{main:e.bandStart,cross:e.low}),o=(0,s.composePoint)(t,{main:e.bandEnd,cross:e.high}),n=Math.min(a.x,o.x)*r.width,d=Math.max(a.x,o.x)*r.width,i=(1-Math.max(a.y,o.y))*r.height,l=(1-Math.min(a.y,o.y))*r.height;return{x:n,y:i,width:d-n,height:l-i}},q=(e,t)=>{const r=e.end>=e.start;return t==="x"?{valueAxis:"y",valueDirectionSign:r?-1:1}:{valueAxis:"x",valueDirectionSign:r?1:-1}},z=1,J={alpha:"unitInterval",fillAlpha:"unitInterval",strokeWidth:"pixels"},x={fill:"color",stroke:"color"},Z={stroke:"color",alpha:"unitInterval",strokeWidth:"pixels",dashArray:"dashArray"},N=class extends s.Geom{constructor(...e){super(...e),this.type="waterfall",this.defaultParams={},this.defaultStat=new K,this.derivedVariables=Object.values(h),this.positionRoles=[{axis:"x",role:"point",valueKind:"value"},{axis:"y",role:"min",valueKind:"value"},{axis:"y",role:"max",valueKind:"value"},{axis:"y",role:"scalar",valueKind:"value",aes:"start"},{axis:"y",role:"scalar",valueKind:"value",aes:"end"}],this.aesthetics=[{kind:"data",name:"y"},{kind:"data",name:"measure"},{kind:"visual",name:"color"}],this.supportedPositions=["identity"],this.supportedCoordTypes=["cartesian","flip"],this.scaleConstraints={discreteMainAxis:!0,zeroBaseline:!0},this.dataLabelCoordTypes=["cartesian","flip"],this.getDataLabelPlacements=X,this.spatialKind="rects",this.highlightStrategy="observation-rerender",this.tooltip=[{key:"Value",aes:"value"},{key:"Running total",aes:"end"}],this.styleTarget=(0,s.defineGeomStyleTarget)({observation:{vocabulary:J,sharedPaint:!1,aesthetics:{},rest:{alpha:1,fillAlpha:1,strokeWidth:0}},parts:{increase:{vocabulary:x,aesthetics:{fill:"color",stroke:"color"},rest:{fill:(0,s.token)("trendPositive"),stroke:(0,s.token)("trendPositive")}},decrease:{vocabulary:x,aesthetics:{fill:"color",stroke:"color"},rest:{fill:(0,s.token)("trendNegative"),stroke:(0,s.token)("trendNegative")}},total:{vocabulary:x,aesthetics:{fill:"color",stroke:"color"},rest:{fill:(0,s.token)("trendNeutral"),stroke:(0,s.token)("trendNeutral")}},connector:{vocabulary:Z,rest:{stroke:(0,s.token)("geomBorder"),alpha:1,strokeWidth:z,dashArray:[]}}}}),this.resolveBandFraction=t=>V(t),this.resolveValueSource=()=>({variable:h.value}),this.validateMapping=({mapping:t,computedVariables:r})=>r.has("end")?[...[...M].filter(a=>t[a]!==void 0).map(a=>({code:"CONFLICTING_STAT_MAPPING",message:`waterfall stat computes '${a}' from 'y'; remove its mapping, or set the layer's stat to 'identity'`})),...t.y===void 0?[{code:"MISSING_AESTHETIC",message:"waterfall stat runs the total through 'y'; map it to the changes",context:{aesthetic:"y"}}]:[]]:[...M].filter(a=>t[a]===void 0).map(a=>({code:"MISSING_AESTHETIC",message:`waterfall geom requires '${a}' when its running totals are already in the data`,context:{aesthetic:a}})),this.resolveAnchorPosition=(t,{coordSystem:r})=>{const a=(0,s.readMainAxis)(r),o=I(t,a);return o===null?null:a==="x"?{x:o.band,y:o.end}:{x:o.end,y:o.band}}}resolveParams(e){const{width:t,connector:r,...a}=super.resolveParams(e);return{...a,...Q(t,e.diagnostics),...ee(r,e.diagnostics)}}compile({data:e,mapping:t,params:r}){const a=S(t.start),o=S(t.end);if(a===null||o===null)return{data:e,mapping:{}};if(!e.hasVariable(a)||!e.hasVariable(o))return{data:e,mapping:{}};const n=V(r)/2;return{data:te(e,a,o,S(t.measure)).addConstantVariable(s.POSITION_VARIABLES.xMin,"numeric",-n).addConstantVariable(s.POSITION_VARIABLES.xMax,"numeric",n),mapping:{y:t.end,value:h.value}}}},Q=(e,t)=>{if(e===void 0)return{};const r=typeof e=="number"&&Number.isFinite(e)?e:null;if(r!==null&&r>0&&r<=1)return{width:r};const a=r!==null&&r>1?1:void 0;return t?.addWarning({code:"INVALID_GEOM_PARAM",message:`Waterfall width must be a fraction in (0, 1], got ${String(e)}; falling back to ${a??"the default band"}.`,context:{geom:"waterfall",param:"width",expected:"0 < width <= 1",actual:r??String(e)},suggestion:"Set params.width to a fraction of the category band in (0, 1]."}),a===void 0?{}:{width:a}},ee=(e,t)=>{if(e===void 0)return{};if(C(e))return{connector:e};const r=B.map(a=>`'${a}'`).join(", ");return t?.addWarning({code:"INVALID_GEOM_PARAM",message:`Waterfall connector must be one of ${r}, got ${String(e)}; falling back to 'spanning'.`,context:{geom:"waterfall",param:"connector",expected:r,actual:String(e)},suggestion:`Set params.connector to one of ${r}.`}),{}},S=e=>e===void 0?null:typeof e=="string"?e:"variable"in e?e.variable:null,te=(e,t,r,a)=>{const o=e.getValues(t,{type:"numeric"}),n=e.getValues(r,{type:"numeric"}),d=re(e,a),i=o.map((l,c)=>{const g=n[c]??null;if(l===null||g===null||!Number.isFinite(l)||!Number.isFinite(g))return null;const p={start:l,end:g};return{...p,kind:d(p,c)}});return e.addVariable(h.kind,"categorical",i.map(l=>l?.kind??null)).addVariable(h.value,"numeric",i.map(l=>l===null?null:l.kind==="total"?l.end:l.end-l.start),e.getValueFormat(r)).addVariable(s.POSITION_VARIABLES.yMin,"numeric",i.map(l=>l===null?null:Math.min(l.start,l.end))).addVariable(s.POSITION_VARIABLES.yMax,"numeric",i.map(l=>l===null?null:Math.max(l.start,l.end)))},re=(e,t)=>{if(e.hasVariable(h.kind)){const a=e.getValues(h.kind);return(o,n)=>ne(a[n])?a[n]:w(o,"relative")}const r=t!==null&&e.hasVariable(t)?_(e.getValues(t),t):[];return(a,o)=>w(a,r[o]??"relative")},ae=new Set(["increase","decrease","total"]),ne=e=>ae.has(e),oe=5e-4,O=(e,t)=>{const r=(e.low+e.high)/2,a=Math.max(e.high-e.low,oe)/2,o=(0,s.composePoint)(t,{main:e.bandStart,cross:r-a}),n=(0,s.composePoint)(t,{main:e.bandEnd,cross:r+a}),[d,i]=P((0,s.toViewBoxX)(o.x),(0,s.toViewBoxX)(n.x)),[l,c]=P((0,s.toViewBoxY)(o.y),(0,s.toViewBoxY)(n.y));return{x:d,y:l,width:i-d,height:c-l}},se=(e,t,r="spanning",a=null)=>{const o=new Map;return e.flatMap(n=>{const d=a===null?null:n.observation[a],i=o.get(d);if(o.set(d,n),i===void 0||!ie(i,n))return[];const l=n.band>=i.band,c=r==="spanning",g=l===c?i.bandStart:i.bandEnd,p=l===c?n.bandEnd:n.bandStart,v=(0,s.composePoint)(t,{main:g,cross:i.end}),m=(0,s.composePoint)(t,{main:p,cross:i.end});return[{from:i,to:n,segment:{x1:(0,s.toViewBoxX)(v.x),y1:(0,s.toViewBoxY)(v.y),x2:(0,s.toViewBoxX)(m.x),y2:(0,s.toViewBoxY)(m.y)}}]})},E=1e-9,ie=(e,t)=>Math.abs(t.start-e.end)<E||Math.abs(t.end-e.end)<E,P=(e,t)=>e<=t?[e,t]:[t,e],le=({layer:e,coordSystem:t,panelRect:r})=>{const a=(0,s.readMainAxis)(t);return y(e.data,a).map(o=>{const n=O(o,a),d=n.x*r.width;return{kind:"region",pathData:`M${d},${n.y*r.height}H${(n.x+n.width)*r.width}V${(n.y+n.height)*r.height}H${d}Z`}})},k=(e,t)=>{const r=(0,s.createStableBandKeyGenerator)(e.data,e.mapping,e.id),a=T(e.mapping.group),o=new Map;return new Map(t.map(n=>{const d=r(n.observation)??e.id,i=a===null?d:`${d}-${String(n.observation[a])}`,l=o.get(i)??0;return o.set(i,l+1),[n,l===0?i:`${i}#${l}`]}))},A=({steps:e,connectors:t=[],mainAxis:r,styleReaders:a,keys:o,isHovered:n=!1,shouldAnimateTransitions:d=!1})=>{const i=n?"hovered":void 0,l=a.part("connector");return(0,u.jsxs)(u.Fragment,{children:[e.map(c=>(0,u.jsx)(ce,{step:c,mainAxis:r,styleReaders:a,state:i,shouldAnimateTransitions:d},o.get(c))),l&&t.map(c=>(0,u.jsx)(de,{connector:c,readers:l,shouldAnimateTransitions:d},`${o.get(c.from)}-connector`))]})},de=({connector:e,readers:t,shouldAnimateTransitions:r})=>{const{observation:a}=e.from,o=t.get("strokeWidth",a)??0,n=t.get("stroke",a);return(0,u.jsx)(s.GeomLine,{"data-testid":"waterfall-connector",x1:(0,s.toPercent)(e.segment.x1),y1:(0,s.toPercent)(e.segment.y1),x2:(0,s.toPercent)(e.segment.x2),y2:(0,s.toPercent)(e.segment.y2),stroke:n===void 0?void 0:(0,s.toPaintColor)(n),strokeOpacity:t.get("alpha",a),strokeWidth:o,strokeDasharray:(0,s.toStrokeDasharray)(t.get("dashArray",a),o),shouldAnimateTransitions:r})},ce=({step:e,mainAxis:t,styleReaders:r,state:a,shouldAnimateTransitions:o})=>{const n=O(e,t),d=r.part(e.kind),i=d?.get("fill",e.observation,a),l=d?.get("stroke",e.observation,a);return(0,u.jsx)(s.GeomRect,{"data-testid":"waterfall-bar",x:(0,s.toPercent)(n.x),y:(0,s.toPercent)(n.y),width:(0,s.toPercent)(n.width),height:(0,s.toPercent)(n.height),fill:i===void 0?void 0:(0,s.toPaintColor)(i),opacity:r.get("alpha",e.observation,a),fillOpacity:r.get("fillAlpha",e.observation,a),stroke:l===void 0?void 0:(0,s.toPaintColor)(l),strokeWidth:r.get("strokeWidth",e.observation,a),shouldAnimateTransitions:o})},ue=({layer:e,sourceLayer:t,mainAxis:r,styleReaders:a})=>{const o=(0,f.useMemo)(()=>y(e.data,r),[e.data,r]);return(0,u.jsx)(A,{steps:o,mainAxis:r,styleReaders:a,keys:(0,f.useMemo)(()=>k(t,o),[t,o])})},he=({layer:e,primary:t,siblings:r,mainAxis:a,styleReaders:o})=>{const n=y([t,...r].map(d=>d.observation),a);return(0,u.jsx)("g",{"data-testid":"hovered-steps",children:(0,u.jsx)(A,{steps:n,mainAxis:a,styleReaders:o,keys:k(e,n),isHovered:!0})})},ge=({layer:e,mainAxis:t,styleReaders:r,shouldAnimateTransitions:a,intro:o})=>{const n=(0,f.useMemo)(()=>y(e.data,t),[e.data,t]),d=$(e.params),i=T(e.mapping.group),l=(0,u.jsx)(A,{steps:n,connectors:(0,f.useMemo)(()=>se(n,t,d,i),[n,t,d,i]),mainAxis:t,styleReaders:r,keys:(0,f.useMemo)(()=>k(e,n),[e,n]),shouldAnimateTransitions:a}),c=pe(o);return c===null?l:(0,u.jsx)(s.IntroFade,{intro:c,children:l})},pe=e=>e==null?null:{type:"fade",layerId:e.layerId,durationSeconds:e.durationSeconds},me=(0,s.defineGeomRenderer)(new N,{coord:"cartesian",swatchShape:"square",guideMode:"band",render:({layer:e,coordSystem:t,styleReaders:r,shouldAnimateTransitions:a,intro:o})=>(0,u.jsx)(ge,{layer:e,mainAxis:(0,s.readMainAxis)(t),styleReaders:r,shouldAnimateTransitions:a,intro:o}),renderHighlight:({layer:e,sourceLayer:t,coordSystem:r,styleReaders:a})=>(0,u.jsx)(ue,{layer:e,sourceLayer:t,mainAxis:(0,s.readMainAxis)(r),styleReaders:a}),renderHover:({layer:e,primary:t,group:r,related:a,coordSystem:o,styleReaders:n})=>(0,u.jsx)(he,{layer:e,primary:t,siblings:[...r,...a],mainAxis:(0,s.readMainAxis)(o),styleReaders:n}),renderHoverCompanions:()=>null,getEditOutlineShapes:le});exports.WaterfallGeom=N;exports.waterfall=me;
@@ -0,0 +1,2 @@
1
+ export { WaterfallGeom } from './engine/waterfall.geom';
2
+ export { waterfall } from './renderer/waterfall';