@graphysdk/geom-funnel 0.0.0-stage → 0.1.1-beta.1791459323341
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 +17 -0
- package/README.md +67 -2
- package/dist/engine/funnel-data-labels.d.ts +7 -0
- package/dist/engine/funnel.geom.d.ts +120 -0
- package/dist/engine/funnel.stat.d.ts +13 -0
- package/dist/engine/utils/measure-stages.d.ts +16 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +478 -0
- package/dist/renderer/funnel-edit-outline.d.ts +3 -0
- package/dist/renderer/funnel-highlight.d.ts +11 -0
- package/dist/renderer/funnel-hover.d.ts +12 -0
- package/dist/renderer/funnel-layer.d.ts +19 -0
- package/dist/renderer/funnel-paint.d.ts +26 -0
- package/dist/renderer/funnel.d.ts +17 -0
- package/dist/renderer/utils/connector-geometry.d.ts +27 -0
- package/dist/renderer/utils/funnel-keys.d.ts +7 -0
- package/dist/utils/funnel-columns.d.ts +10 -0
- package/dist/utils/funnel-params.d.ts +6 -0
- package/dist/utils/read-funnel.d.ts +21 -0
- package/dist/utils/stage-geometry.d.ts +11 -0
- package/licenses/PolyForm-Free-Trial-1.0.0.md +93 -0
- package/licenses/PolyForm-NonCommercial-1.0.0.md +131 -0
- package/licenses/PolyForm-Small-Business-1.0.0.md +121 -0
- package/package.json +71 -3
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,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @graphysdk/geom-funnel
|
|
2
2
|
|
|
3
|
-
This
|
|
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 funnel geom for the Graphy charting SDK. It shows how many of a starting count reach each stage of a process: one bar per stage, centred on a common line and as long as the stage's count, with a band joining each bar to the next so the narrowing reads as one shape.
|
|
6
|
+
|
|
7
|
+
Learn more at [graphy.dev](https://graphy.dev).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @graphysdk/geom-funnel @graphysdk/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires React 19. Its only peer is `@graphysdk/react`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Data is one row per stage, in the order the funnel runs. Map the stage to `x` and its count to `y`. The classic funnel, stages running down, is `coord.flip()`, and the value axis measures from the centre line, so hide it:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { createGraphyKit, GraphRenderer } from '@graphysdk/react';
|
|
23
|
+
import { funnel } from '@graphysdk/geom-funnel';
|
|
24
|
+
|
|
25
|
+
const kit = createGraphyKit({ plugins: [funnel] });
|
|
26
|
+
|
|
27
|
+
const data = {
|
|
28
|
+
columns: [{ key: 'stage' }, { key: 'visitors' }],
|
|
29
|
+
rows: [
|
|
30
|
+
{ stage: 'Visited', visitors: 10000 },
|
|
31
|
+
{ stage: 'Added to cart', visitors: 3000 },
|
|
32
|
+
{ stage: 'Paid', visitors: 1200 },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const spec = kit.pipe(
|
|
37
|
+
kit.createSpec({ x: 'stage', y: 'visitors' }),
|
|
38
|
+
kit.geom.funnel({ dataLabels: { showDataLabels: true } }),
|
|
39
|
+
kit.scale.x.discrete(),
|
|
40
|
+
kit.scale.y.continuous(),
|
|
41
|
+
kit.coord.flip(),
|
|
42
|
+
kit.config({ axes: { y: { isVisible: false } } })
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export function Checkout() {
|
|
46
|
+
return (
|
|
47
|
+
<kit.GraphProvider spec={spec} data={data}>
|
|
48
|
+
<GraphRenderer />
|
|
49
|
+
</kit.GraphProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The geom's default `funnel` stat writes each stage's share of the first stage to `funnelPercentOfFirst` and of the stage before it to `funnelPercentOfPrevious`. Map `label` to either to print it, or name it in a `highlight`. Under `stat: kit.stat.identity()`, map `percentOfFirst` and `percentOfPrevious` to shares already in your data. `params.width` sets the fraction of its band a bar spans (`0.7` by default). Colour the bars with `kit.style.geom.funnel()` and the bands between them with `kit.style.geom.funnel.connector()`.
|
|
55
|
+
|
|
56
|
+
## Docs
|
|
57
|
+
|
|
58
|
+
- [Funnel](https://docs.graphy.dev/sdk-next/graph-types/funnel)
|
|
59
|
+
|
|
60
|
+
## Feedback
|
|
61
|
+
|
|
62
|
+
This geom is released early to hear what works. Tell us on [Discord](https://discord.gg/yGmYCjkSr).
|
|
63
|
+
|
|
64
|
+
## Licence
|
|
65
|
+
|
|
66
|
+
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).
|
|
67
|
+
|
|
68
|
+
For a commercial licence, [talk to us](https://graphy.dev/license).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PlacementResult, PluginPlacementContext } from '@graphysdk/react';
|
|
2
|
+
/**
|
|
3
|
+
* A label per stage. Under `'auto'` it sits in the middle of the bar, where a funnel is read, and moves past the
|
|
4
|
+
* bar's end when the bar is too short to hold it; an explicit `position` is honoured as written. Stack totals and
|
|
5
|
+
* category labels are bar vocabulary; a funnel places neither.
|
|
6
|
+
*/
|
|
7
|
+
export declare const placeFunnelDataLabels: (context: PluginPlacementContext) => PlacementResult;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { AnchorContext, AnchorPosition, GeomCompileResult, GeomCompilerInput, GeomMappingValidationInput, GeomObservationPart, GeomStylePart, GeomStyleTarget, Observation, PlacementResult, PluginPlacementContext, UserInputIssue, Geom } from '@graphysdk/react';
|
|
2
|
+
import { FunnelGeomParams } from '../utils/funnel-params';
|
|
3
|
+
import { FunnelStat } from './funnel.stat';
|
|
4
|
+
/** The observations paint the shared geom vocabulary on top of these. */
|
|
5
|
+
declare const BAR_VOCABULARY: {
|
|
6
|
+
readonly strokeWidth: "pixels";
|
|
7
|
+
};
|
|
8
|
+
declare const CONNECTOR_VOCABULARY: {
|
|
9
|
+
readonly fill: "color";
|
|
10
|
+
readonly fillAlpha: "unitInterval";
|
|
11
|
+
readonly stroke: "color";
|
|
12
|
+
readonly strokeWidth: "pixels";
|
|
13
|
+
};
|
|
14
|
+
/** Named so the published types reach the observations and every part through `@graphysdk/react`, the package's one peer. */
|
|
15
|
+
interface FunnelStyleTarget extends GeomStyleTarget {
|
|
16
|
+
observation: GeomObservationPart<typeof BAR_VOCABULARY>;
|
|
17
|
+
parts: {
|
|
18
|
+
connector: GeomStylePart<typeof CONNECTOR_VOCABULARY>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A funnel: how many of a starting count reach each stage of a process, one bar per stage centred on a common
|
|
23
|
+
* line, its length the stage's value, with a band joining each bar to the next so the narrowing reads as one
|
|
24
|
+
* shape.
|
|
25
|
+
*
|
|
26
|
+
* By default data is one row per stage, in the order the funnel runs: `x` is the stage and `y` its count, and the
|
|
27
|
+
* `funnel` stat writes each stage's share of the first and of the one before it. Under `stat: 'identity'` the
|
|
28
|
+
* author may map `percentOfFirst` and `percentOfPrevious` to shares already in the data. The classic funnel,
|
|
29
|
+
* stages running down, is `coord.flip()`.
|
|
30
|
+
*/
|
|
31
|
+
export declare class FunnelGeom extends Geom<FunnelGeomParams> {
|
|
32
|
+
readonly type: "funnel";
|
|
33
|
+
readonly defaultParams: FunnelGeomParams;
|
|
34
|
+
readonly defaultStat: FunnelStat;
|
|
35
|
+
/** A bar spans the `y` interval either side of zero, so both ends come from `y` and the axis centres on zero. */
|
|
36
|
+
readonly positionRoles: readonly [{
|
|
37
|
+
readonly axis: "x";
|
|
38
|
+
readonly role: "point";
|
|
39
|
+
readonly valueKind: "value";
|
|
40
|
+
}, {
|
|
41
|
+
readonly axis: "y";
|
|
42
|
+
readonly role: "min";
|
|
43
|
+
readonly valueKind: "value";
|
|
44
|
+
readonly aes: "y";
|
|
45
|
+
}, {
|
|
46
|
+
readonly axis: "y";
|
|
47
|
+
readonly role: "max";
|
|
48
|
+
readonly valueKind: "value";
|
|
49
|
+
readonly aes: "y";
|
|
50
|
+
}];
|
|
51
|
+
readonly aesthetics: readonly [{
|
|
52
|
+
readonly kind: "visual";
|
|
53
|
+
readonly name: "color";
|
|
54
|
+
}, {
|
|
55
|
+
readonly kind: "data";
|
|
56
|
+
readonly name: "percentOfFirst";
|
|
57
|
+
}, {
|
|
58
|
+
readonly kind: "data";
|
|
59
|
+
readonly name: "percentOfPrevious";
|
|
60
|
+
}];
|
|
61
|
+
/** The shares the stat writes, which an author may name in a label, a colour or a style entry's `where`. */
|
|
62
|
+
readonly derivedVariables: ("funnelPercentOfFirst" | "funnelPercentOfPrevious")[];
|
|
63
|
+
/** One bar per stage, all on one centre line: a stacked or dodged stage would leave it. */
|
|
64
|
+
readonly supportedPositions: readonly ["identity"];
|
|
65
|
+
readonly supportedCoordTypes: readonly ["cartesian", "flip"];
|
|
66
|
+
readonly scaleConstraints: {
|
|
67
|
+
discreteMainAxis: boolean;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A bar is measured from the centre line out, so a grid line on the value axis marks nothing a reader wants,
|
|
71
|
+
* and one along the stages would cut through the connectors.
|
|
72
|
+
*/
|
|
73
|
+
readonly grid: {
|
|
74
|
+
cartesian: {
|
|
75
|
+
hideGridX: boolean;
|
|
76
|
+
hideGridY: boolean;
|
|
77
|
+
};
|
|
78
|
+
flip: {
|
|
79
|
+
hideGridX: boolean;
|
|
80
|
+
hideGridY: boolean;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
readonly legend: {
|
|
84
|
+
suppressWhenSingleItem: boolean;
|
|
85
|
+
};
|
|
86
|
+
readonly dataLabelCoordTypes: readonly ["cartesian", "flip"];
|
|
87
|
+
readonly getDataLabelPlacements: (context: PluginPlacementContext) => PlacementResult;
|
|
88
|
+
/**
|
|
89
|
+
* A hovered stage is one unit: its bar is a rectangle, so a press anywhere across it is on the stage, and the
|
|
90
|
+
* whole of its band takes a press however short its bar.
|
|
91
|
+
*/
|
|
92
|
+
readonly spatialKind = "rects";
|
|
93
|
+
readonly highlightStrategy: "observation-rerender";
|
|
94
|
+
/** The stage's count, labelled like the axis, then its conversion from the first stage and from the last. */
|
|
95
|
+
readonly tooltip: ({
|
|
96
|
+
aes: string;
|
|
97
|
+
key?: undefined;
|
|
98
|
+
} | {
|
|
99
|
+
key: string;
|
|
100
|
+
aes: string;
|
|
101
|
+
})[];
|
|
102
|
+
readonly styleTarget: FunnelStyleTarget;
|
|
103
|
+
/** A bar spans `width` of its band, so an axis annotation aligned to an edge lands where the bars end. */
|
|
104
|
+
readonly resolveBandFraction: Geom['resolveBandFraction'];
|
|
105
|
+
/**
|
|
106
|
+
* Substitutes an undrawable width and reports it, so the same spec can't diverge across renderers. Above `1`
|
|
107
|
+
* it would overlap the neighbouring bars, so it clamps to the full band; at or below `0` nothing is drawable,
|
|
108
|
+
* so the default takes over.
|
|
109
|
+
*/
|
|
110
|
+
resolveParams(options: Parameters<Geom['resolveParams']>[0]): Record<string, unknown>;
|
|
111
|
+
/** Under the `funnel` stat the shares come from `y`, so a mapping of their own would be overwritten. */
|
|
112
|
+
readonly validateMapping: ({ mapping, computedVariables, }: GeomMappingValidationInput) => readonly UserInputIssue[];
|
|
113
|
+
/**
|
|
114
|
+
* Where the editor raises its annotation trigger and where an annotation on a stage lands: at the end of its
|
|
115
|
+
* bar, at the band centre. The centre line is shared by every stage; the end is where the stage's length reads.
|
|
116
|
+
*/
|
|
117
|
+
readonly resolveAnchorPosition: (observation: Observation, { coordSystem }: AnchorContext) => AnchorPosition | null;
|
|
118
|
+
compile({ data, mapping, params }: GeomCompilerInput): GeomCompileResult;
|
|
119
|
+
}
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StatCompileResult, StatCompilerInput, Stat } from '@graphysdk/react';
|
|
2
|
+
/** Every aesthetic the shares provide, so a layer under this stat maps none of them itself. */
|
|
3
|
+
export declare const FUNNEL_STAT_AESTHETICS: ReadonlySet<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Writes each stage's share of the first stage and of the one before it, from the counts in `y`, in data order.
|
|
6
|
+
* Only an explicit `group` splits the funnel: a discrete `color` usually paints the stages and must not break one
|
|
7
|
+
* funnel into several. A stage may appear once per funnel, since a second row would draw a second bar over it.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FunnelStat extends Stat {
|
|
10
|
+
readonly type: "funnel";
|
|
11
|
+
readonly computedVariables: ReadonlySet<string>;
|
|
12
|
+
protected computeStat({ data, mapping }: StatCompilerInput): StatCompileResult;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One stage's shares, as fractions: `0.25` is a quarter. */
|
|
2
|
+
export interface StageShares {
|
|
3
|
+
percentOfFirst: number | null;
|
|
4
|
+
percentOfPrevious: number | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Reads a stage's value as what it draws: a count, so a missing, negative or non-finite value draws nothing
|
|
8
|
+
* and leaves the stages around it to meet across the gap.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readStageValue: (value: number | null) => number | null;
|
|
11
|
+
/**
|
|
12
|
+
* Each stage's share of the first stage and of the one before it, in data order, which is the order the
|
|
13
|
+
* funnel runs in. A stage that draws nothing has no shares and is skipped as the next one's previous; a share
|
|
14
|
+
* of a stage of zero is `null`, since it has no ratio.
|
|
15
|
+
*/
|
|
16
|
+
export declare const measureStages: (values: ReadonlyArray<number | null>) => StageShares[];
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let o=require("@graphysdk/react"),p=require("react"),c=require("react/jsx-runtime");var g={percentOfFirst:"funnelPercentOfFirst",percentOfPrevious:"funnelPercentOfPrevious"},j=.7,M=({width:e})=>e??j,E=(e,r)=>{const t=r==="x"?(0,o.getX)(e):(0,o.getY)(e),n=r==="x"?(0,o.getXMin)(e):(0,o.getYMin)(e),a=r==="x"?(0,o.getXMax)(e):(0,o.getYMax)(e),i=r==="x"?(0,o.getYMin)(e):(0,o.getXMin)(e),s=r==="x"?(0,o.getYMax)(e):(0,o.getXMax)(e);return t===null||n===null||a===null||i===null||s===null?null:{observation:e,band:t,bandStart:n,bandEnd:a,low:i,high:s}},v=(e,r)=>{const t=[];for(const n of e){const a=E(n,r);a!==null&&t.push(a)}return t},N=e=>e===null||!Number.isFinite(e)||e<0?null:e,$=e=>{let r=null,t=null;return e.map(n=>{const a=N(n);if(a===null)return{percentOfFirst:null,percentOfPrevious:null};r??=a;const i={percentOfFirst:V(a,r),percentOfPrevious:t===null?1:V(a,t)};return t=a,i})},V=(e,r)=>r===0?null:e/r,B={percentOfFirst:g.percentOfFirst,percentOfPrevious:g.percentOfPrevious},T=new Set(Object.keys(B)),F={percentOfFirst:null,percentOfPrevious:null},H=class extends o.Stat{constructor(...e){super(...e),this.type="funnel",this.computedVariables=T}computeStat({data:e,mapping:r}){const t=(0,o.readVariableName)(r.y);if(t===null||!e.hasVariable(t))return{data:e,mapping:{}};if(e.size()>0&&e.getType(t)!=="numeric"){const s=e.getType(t);throw new o.UserInputError({code:"INCOMPATIBLE_TYPE",message:`'funnel' stat requires a numeric 'y' (got '${s}' for variable '${t}')`,context:{aesthetic:"y",variableName:t,variableType:s},suggestion:"Map each stage's count to 'y', or its shares to 'percentOfFirst' and 'percentOfPrevious' under stat 'identity'."})}const n=(0,o.readVariableName)(r.group),a=D(e,n!==null&&e.hasVariable(n)?[n]:[]),i=(0,o.readVariableName)(r.x);return i!==null&&e.hasVariable(i)&&R(e,i,a),{data:W(e,t,a),mapping:B}}},W=(e,r,t)=>{const n=e.getValues(r,{type:"numeric"}),a=Y(n,t),i={type:"percentage"};return e.addVariable(g.percentOfFirst,"numeric",a.map(s=>s.percentOfFirst),i).addVariable(g.percentOfPrevious,"numeric",a.map(s=>s.percentOfPrevious),i)},Y=(e,r)=>{const t=e.map(()=>F);for(const n of _(r).values()){const a=$(n.map(i=>e[i]??null));n.forEach((i,s)=>{t[i]=a[s]??F})}return t},R=(e,r,t)=>{const n=e.getValues(r);for(const a of _(t).values()){const i=new Set;for(const s of a){const l=String(n[s]);if(i.has(l))throw new o.UserInputError({code:"INVALID_DATA_SHAPE",message:`funnel stage '${l}' appears more than once in one funnel (variable '${r}')`,context:{aesthetic:"x",variableName:r},suggestion:"Aggregate to one row per stage, or map 'group' to the variable that tells the funnels apart."});i.add(l)}}},_=e=>{const r=new Map;return e.forEach((t,n)=>{const a=r.get(t);a===void 0?r.set(t,[n]):a.push(n)}),r},D=(e,r)=>{const t=r.map(n=>e.getValues(n));return Array.from({length:e.size()},(n,a)=>JSON.stringify(t.map(i=>i[a])))},K=5e-4,S=(e,r)=>{const t=(e.low+e.high)/2,n=Math.max(e.high-e.low,K)/2,a=(0,o.composePoint)(r,{main:e.bandStart,cross:t-n}),i=(0,o.composePoint)(r,{main:e.bandEnd,cross:t+n}),[s,l]=I((0,o.toViewBoxX)(a.x),(0,o.toViewBoxX)(i.x)),[u,h]=I((0,o.toViewBoxY)(a.y),(0,o.toViewBoxY)(i.y));return{x:s,y:u,width:l-s,height:h-u}},I=(e,r)=>e<=r?[e,r]:[r,e],X=e=>{const r=[];if(!e.layer.dataLabels.showDataLabels)return{labels:r,droppedCount:0};const{position:t,justify:n,align:a,offset:i}=e.layer.dataLabels,s=(0,o.readMainAxis)(e.coordSystem),l=(0,o.buildPanelAnchorBox)(e.panelRect),{width:u,height:h}=e.panelRect;for(const d of v(e.layer.data,s)){const y=e.formatLabel(d.observation);if(y==="")continue;const m=S(d,s),C={x:m.x*u,y:m.y*h,width:m.width*u,height:m.height*h},x=(A,L,G)=>{const b=e.measureDataLabel("observation",A,y);return{...(0,o.resolveAnchoredLabelCenter)({box:C,panel:l,orientation:U(s),placement:A,justify:L,align:G,offset:i,labelWidth:b.width,labelHeight:b.height}),width:b.width,height:b.height}},w=t==="auto"?x("inside","center","center"):null,f=w===null?x(t==="auto"?"outside":t,n,a):w.position==="inside"?w:x("outside","end","center");r.push({layerId:e.layer.id,x:f.x,y:f.y,text:y,width:f.width,height:f.height,isRotated:!1,role:"observation",position:f.position})}return{labels:r,droppedCount:0}},U=e=>e==="x"?{valueAxis:"y",valueDirectionSign:-1}:{valueAxis:"x",valueDirectionSign:1},q=.25,Z={strokeWidth:"pixels"},z={fill:"color",fillAlpha:"unitInterval",stroke:"color",strokeWidth:"pixels"},k=class extends o.Geom{constructor(...e){super(...e),this.type="funnel",this.defaultParams={},this.defaultStat=new H,this.positionRoles=[{axis:"x",role:"point",valueKind:"value"},{axis:"y",role:"min",valueKind:"value",aes:"y"},{axis:"y",role:"max",valueKind:"value",aes:"y"}],this.aesthetics=[{kind:"visual",name:"color"},{kind:"data",name:"percentOfFirst"},{kind:"data",name:"percentOfPrevious"}],this.derivedVariables=[g.percentOfFirst,g.percentOfPrevious],this.supportedPositions=["identity"],this.supportedCoordTypes=["cartesian","flip"],this.scaleConstraints={discreteMainAxis:!0},this.grid={cartesian:{hideGridX:!0,hideGridY:!0},flip:{hideGridX:!0,hideGridY:!0}},this.legend={suppressWhenSingleItem:!0},this.dataLabelCoordTypes=["cartesian","flip"],this.getDataLabelPlacements=X,this.spatialKind="rects",this.highlightStrategy="observation-rerender",this.tooltip=[{aes:"y"},{key:"% of first",aes:"percentOfFirst"},{key:"% of previous",aes:"percentOfPrevious"}],this.styleTarget=(0,o.defineGeomStyleTarget)({observation:{vocabulary:Z,aesthetics:{fill:"color",alpha:"alpha"},rest:{fill:(0,o.token)("geom"),fillAlpha:1,strokeWidth:0}},parts:{connector:{vocabulary:z,aesthetics:{fill:"color"},rest:{fill:(0,o.token)("geom"),fillAlpha:q,stroke:(0,o.token)("geom"),strokeWidth:0}}}}),this.resolveBandFraction=r=>M(r),this.validateMapping=({mapping:r,computedVariables:t})=>t.has("percentOfFirst")?[...T].filter(n=>r[n]!==void 0).map(n=>({code:"CONFLICTING_STAT_MAPPING",message:`funnel stat computes '${n}' from 'y'; remove its mapping, or set the layer's stat to 'identity'`})):[],this.resolveAnchorPosition=(r,{coordSystem:t})=>{const n=(0,o.readMainAxis)(t),a=E(r,n);return a===null?null:n==="x"?{x:a.band,y:a.high}:{x:a.high,y:a.band}}}resolveParams(e){const{width:r,...t}=super.resolveParams(e);return{...t,...J(r,e.diagnostics)}}compile({data:e,mapping:r,params:t}){const n=(0,o.readVariableName)(r.y);if(n===null||!e.hasVariable(n))return{data:e,mapping:{}};const a=M(t)/2;return{data:Q(e,n).addConstantVariable(o.POSITION_VARIABLES.xMin,"numeric",-a).addConstantVariable(o.POSITION_VARIABLES.xMax,"numeric",a),mapping:{}}}},J=(e,r)=>{if(e===void 0)return{};const t=typeof e=="number"&&Number.isFinite(e)?e:null;if(t!==null&&t>0&&t<=1)return{width:t};const n=t!==null&&t>1?1:void 0;return r?.addWarning({code:"INVALID_GEOM_PARAM",message:`Funnel width must be a fraction in (0, 1], got ${String(e)}; falling back to ${n??"the default band"}.`,context:{geom:"funnel",param:"width",expected:"0 < width <= 1",actual:t??String(e)},suggestion:"Set params.width to a fraction of the stage band in (0, 1]."}),n===void 0?{}:{width:n}},Q=(e,r)=>{const t=e.getValues(r,{type:"numeric"}).map(N);return e.addVariable(o.POSITION_VARIABLES.yMin,"numeric",t.map(n=>n===null?null:-n/2)).addVariable(o.POSITION_VARIABLES.yMax,"numeric",t.map(n=>n===null?null:n/2))},ee=({layer:e,coordSystem:r,panelRect:t})=>{const n=(0,o.readMainAxis)(r);return v(e.data,n).map(a=>{const i=S(a,n),s=i.x*t.width;return{kind:"region",pathData:`M${s},${i.y*t.height}H${(i.x+i.width)*t.width}V${(i.y+i.height)*t.height}H${s}Z`}})},P=e=>{const r=(0,o.createStableKeyGenerator)(e.data,e.mapping,e.id);return t=>r(t.observation)},te=(e,r,t,n=null)=>re(e,n).flatMap(a=>{const i=ne(a);return i.slice(1).flatMap((s,l)=>{const u=i[l];return u===void 0?[]:[ae(u,s,r,t)]})}),re=(e,r)=>{if(r===null)return[[...e]];const t=new Map;for(const n of e){const a=n.observation[r],i=t.get(a);i===void 0?t.set(a,[n]):i.push(n)}return[...t.values()]},ne=e=>{const r=e[0],t=e[e.length-1],n=r!==void 0&&t!==void 0&&r.band>t.band?-1:1;return[...e].sort((a,i)=>(a.band-i.band)*n)},ae=(e,r,t,n)=>{const a=r.band>=e.band,i=a?e.bandEnd:e.bandStart,s=a?r.bandStart:r.bandEnd,l=(u,h)=>{const d=(0,o.composePoint)(t,{main:u,cross:h});return{x:(0,o.toViewBoxX)(d.x)*n.width,y:(0,o.toViewBoxY)(d.y)*n.height}};return{from:e,to:r,corners:[l(i,e.low),l(i,e.high),l(s,r.high),l(s,r.low)]}},ie=({corners:e})=>`${e.map((r,t)=>`${t===0?"M":"L"}${r.x},${r.y}`).join("")}Z`,O=({stages:e,connectors:r=[],connectorSource:t,mainAxis:n,styleReaders:a,getKey:i,isHovered:s=!1,shouldAnimateTransitions:l=!1})=>{const u=s?"hovered":void 0,h=a.part("connector");return(0,c.jsxs)(c.Fragment,{children:[h&&t!==void 0&&r.map(d=>(0,c.jsx)(oe,{connector:d,source:t,readers:h,shouldAnimateTransitions:l},`${i(d.from)}-connector`)),e.map(d=>(0,c.jsx)(se,{stage:d,mainAxis:n,styleReaders:a,state:u,shouldAnimateTransitions:l},i(d)))]})},oe=({connector:e,source:r,readers:t,shouldAnimateTransitions:n})=>{const{observation:a}=e.from,i=t.get("fill",a),s=t.get("stroke",a);return(0,c.jsx)(o.GeomPath,{"data-testid":"funnel-connector",d:ie(e),source:r,fill:i===void 0?void 0:(0,o.toPaintColor)(i),fillOpacity:t.get("fillAlpha",a),stroke:s===void 0?void 0:(0,o.toPaintColor)(s),strokeWidth:t.get("strokeWidth",a),shouldAnimateTransitions:n})},se=({stage:e,mainAxis:r,styleReaders:t,state:n,shouldAnimateTransitions:a})=>{const i=S(e,r),s=t.get("fill",e.observation,n),l=t.get("stroke",e.observation,n);return(0,c.jsx)(o.GeomRect,{"data-testid":"funnel-bar",x:(0,o.toPercent)(i.x),y:(0,o.toPercent)(i.y),width:(0,o.toPercent)(i.width),height:(0,o.toPercent)(i.height),fill:s===void 0?void 0:(0,o.toPaintColor)(s),opacity:t.get("alpha",e.observation,n),fillOpacity:t.get("fillAlpha",e.observation,n),stroke:l===void 0?void 0:(0,o.toPaintColor)(l),strokeWidth:t.get("strokeWidth",e.observation,n),shouldAnimateTransitions:a})},le=({layer:e,sourceLayer:r,mainAxis:t,styleReaders:n})=>(0,c.jsx)(O,{stages:(0,p.useMemo)(()=>v(e.data,t),[e.data,t]),mainAxis:t,styleReaders:n,getKey:(0,p.useMemo)(()=>P(r),[r])}),ue=({layer:e,primary:r,siblings:t,mainAxis:n,styleReaders:a})=>(0,c.jsx)("g",{"data-testid":"hovered-stages",children:(0,c.jsx)(O,{stages:v([r,...t].map(i=>i.observation),n),mainAxis:n,styleReaders:a,getKey:(0,p.useMemo)(()=>P(e),[e]),isHovered:!0})}),de=({layer:e,mainAxis:r,panelWidth:t,panelHeight:n,styleReaders:a,shouldAnimateTransitions:i,intro:s})=>{const l=(0,p.useMemo)(()=>v(e.data,r),[e.data,r]),u=(0,o.readVariableName)(e.mapping.group),h=(0,c.jsx)(O,{stages:l,connectors:(0,p.useMemo)(()=>te(l,r,{width:t,height:n},u),[l,r,t,n,u]),connectorSource:e,mainAxis:r,styleReaders:a,getKey:(0,p.useMemo)(()=>P(e),[e]),shouldAnimateTransitions:i}),d=ce(s);return d===null?h:(0,c.jsx)(o.IntroFade,{intro:d,children:h})},ce=e=>e==null?null:{type:"fade",layerId:e.layerId,durationSeconds:e.durationSeconds},he=(0,o.defineGeomRenderer)(new k,{coord:"cartesian",swatchShape:"square",guideMode:"band",render:({layer:e,coordSystem:r,panelRect:t,styleReaders:n,shouldAnimateTransitions:a,intro:i})=>(0,c.jsx)(de,{layer:e,mainAxis:(0,o.readMainAxis)(r),panelWidth:t.width,panelHeight:t.height,styleReaders:n,shouldAnimateTransitions:a,intro:i}),renderHighlight:({layer:e,sourceLayer:r,coordSystem:t,styleReaders:n})=>(0,c.jsx)(le,{layer:e,sourceLayer:r,mainAxis:(0,o.readMainAxis)(t),styleReaders:n}),renderHover:({layer:e,primary:r,group:t,related:n,coordSystem:a,styleReaders:i})=>(0,c.jsx)(ue,{layer:e,primary:r,siblings:[...t,...n],mainAxis:(0,o.readMainAxis)(a),styleReaders:i}),renderHoverCompanions:()=>null,getEditOutlineShapes:ee});exports.FunnelGeom=k;exports.funnel=he;
|
package/dist/index.d.ts
ADDED