@graphysdk/geom-sankey 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,50 @@
1
- # Temporary Holding Version
1
+ # @graphysdk/geom-sankey
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, params, style or look. Use a caret range (`^0.1.0`) to take only patches.
4
+
5
+ A sankey geom for the Graphy charting SDK. It draws flows between nodes, each flow as wide as its amount, so what runs through a system and where it ends up read at a glance.
6
+
7
+ Learn more at [graphy.dev](https://graphy.dev).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @graphysdk/geom-sankey @graphysdk/react
13
+ ```
14
+
15
+ Requires React 19. Its only peer is `@graphysdk/react`.
16
+
17
+ ## Usage
18
+
19
+ Data is one row per flow. Map the node it leaves to `source`, the node it reaches to `target` and its amount to `value`. Each node takes its own color, and each flow the color of the node it leaves. Map `color` to the target column (`color: 'to'`) to color a flow as the node it reaches instead:
20
+
21
+ ```tsx
22
+ import { createGraphyKit, GraphRenderer } from '@graphysdk/react';
23
+ import { sankey } from '@graphysdk/geom-sankey';
24
+
25
+ const kit = createGraphyKit({ plugins: [sankey] });
26
+
27
+ const spec = kit.pipe(kit.createSpec({ source: 'from', target: 'to', value: 'twh' }), kit.geom.sankey());
28
+
29
+ export function Energy({ data }) {
30
+ return (
31
+ <kit.GraphProvider spec={spec} data={data}>
32
+ <GraphRenderer />
33
+ </kit.GraphProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ The geom lays itself out, so it takes no scales and draws no axes. Style the flows with `kit.style.geom.sankey()`, the nodes with `kit.style.geom.sankey.node()` and their names with `kit.style.geom.sankey.label()`.
39
+
40
+ ## Docs
41
+
42
+ - [Sankey](https://docs.graphy.dev/sdk-next/graph-types/sankey)
43
+
44
+ ## Feedback
45
+
46
+ This geom is released early to hear what works. Tell us on [Discord](https://discord.gg/yGmYCjkSr).
47
+
48
+ ## Licence
49
+
50
+ 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).
@@ -0,0 +1,39 @@
1
+ /** Which column a node sits in: d3-sankey's alignments. */
2
+ export type SankeyAlign = 'justify' | 'left' | 'right' | 'center';
3
+ /** One flow to lay out, its ends named by their node's key. */
4
+ export interface SankeyLink {
5
+ source: string;
6
+ target: string;
7
+ value: number;
8
+ }
9
+ /** A node's box in unit space, top-left origin, and the total that runs through it. */
10
+ export interface SankeyNodeBox {
11
+ key: string;
12
+ value: number;
13
+ x0: number;
14
+ x1: number;
15
+ y0: number;
16
+ y1: number;
17
+ }
18
+ /** A flow's band at each end, in unit space: it leaves its source at `x0` and reaches its target at `x1`. */
19
+ export interface SankeyFlowBand<Link extends SankeyLink = SankeyLink> {
20
+ /** The link the flow draws, as the layout was given it. */
21
+ link: Link;
22
+ x0: number;
23
+ x1: number;
24
+ y0: number;
25
+ y1: number;
26
+ targetY0: number;
27
+ targetY1: number;
28
+ }
29
+ export interface SankeyLayout<Link extends SankeyLink = SankeyLink> {
30
+ /** In the order each node's first link names it. */
31
+ nodes: SankeyNodeBox[];
32
+ /** One per link the layout could draw, in the links' order. */
33
+ flows: Array<SankeyFlowBand<Link>>;
34
+ }
35
+ /**
36
+ * Lays a sankey out in unit space, so the geometry holds at any panel size. A link that would close a loop, a
37
+ * self-link included, is dropped: d3-sankey throws on a cycle.
38
+ */
39
+ export declare const computeSankeyLayout: <Link extends SankeyLink>(links: readonly Link[], align: SankeyAlign) => SankeyLayout<Link>;
@@ -0,0 +1,100 @@
1
+ import { AnchorPosition, GeomCompileResult, GeomCompilerInput, GeomMappingValidationInput, GeomObservationPart, GeomStylePart, GeomStyleTarget, Observation, UserInputIssue, Geom } from '@graphysdk/react';
2
+ import { SankeyStat } from './sankey.stat';
3
+ import { SankeyAlign } from './sankey-layout';
4
+ /** A flow is one filled ribbon, so it paints the shared geom vocabulary alone. */
5
+ declare const FLOW_VOCABULARY: {};
6
+ declare const NODE_VOCABULARY: {
7
+ readonly fill: "paint";
8
+ readonly fillAlpha: "unitInterval";
9
+ readonly alpha: "unitInterval";
10
+ readonly stroke: "color";
11
+ readonly strokeWidth: "pixels";
12
+ };
13
+ declare const LABEL_VOCABULARY: {
14
+ readonly fontFamily: "fontFamily";
15
+ readonly fontSize: "pixels";
16
+ readonly fontWeight: "fontWeight";
17
+ readonly textColor: "color";
18
+ readonly textOutlineColor: "color";
19
+ readonly textOutlineWidth: "pixels";
20
+ };
21
+ /** Named so the published types reach the vocabularies through `@graphysdk/react`, the package's one peer. */
22
+ interface SankeyStyleTarget extends GeomStyleTarget {
23
+ observation: GeomObservationPart<typeof FLOW_VOCABULARY>;
24
+ parts: {
25
+ node: GeomStylePart<typeof NODE_VOCABULARY>;
26
+ label: GeomStylePart<typeof LABEL_VOCABULARY>;
27
+ };
28
+ }
29
+ export interface SankeyParams {
30
+ /** Which column a node sits in. `justify`, the default, sends a node no flow leaves to the last column. */
31
+ align: SankeyAlign;
32
+ }
33
+ /**
34
+ * A sankey diagram: flows between nodes, each as wide as its amount.
35
+ *
36
+ * Data is one row per flow: `source` names the node it leaves, `target` the node it reaches, and `value` its
37
+ * amount. The nodes are the names the flows share, each as tall as the larger of what flows into and out of it.
38
+ * The geom lays itself out across the panel, so it draws no axes and takes no position scales.
39
+ */
40
+ export declare class SankeyGeom extends Geom<SankeyParams> {
41
+ readonly type: "sankey";
42
+ readonly defaultParams: SankeyParams;
43
+ readonly defaultStat: SankeyStat;
44
+ readonly aesthetics: readonly [{
45
+ readonly kind: "data";
46
+ readonly name: "source";
47
+ readonly required: true;
48
+ }, {
49
+ readonly kind: "data";
50
+ readonly name: "target";
51
+ readonly required: true;
52
+ }, {
53
+ readonly kind: "data";
54
+ readonly name: "value";
55
+ readonly required: true;
56
+ }, {
57
+ readonly kind: "data";
58
+ readonly name: "node";
59
+ }, {
60
+ readonly kind: "visual";
61
+ readonly name: "color";
62
+ }, {
63
+ readonly kind: "visual";
64
+ readonly name: "alpha";
65
+ }];
66
+ readonly identityKey: {
67
+ variable: "sankeyObservationKey";
68
+ };
69
+ /** The layout places every observation, so nothing stacks or dodges on top. */
70
+ readonly supportedPositions: readonly ["identity"];
71
+ /** The layout runs left to right across the panel; there is no axis to flip. */
72
+ readonly supportedCoordTypes: readonly ["cartesian"];
73
+ /** Every node carries its name beside it, and color can only group by node, so a legend would only repeat them. */
74
+ readonly legend: {
75
+ readonly defaultPosition: "none";
76
+ };
77
+ readonly spatialKind = "render-hit-test";
78
+ readonly highlightStrategy: "observation-rerender";
79
+ /** A flow is headed by the node it leaves. A node has no target, so it gets no row for one. */
80
+ readonly tooltip: readonly [{
81
+ readonly aes: "node";
82
+ readonly heading: true;
83
+ }, {
84
+ readonly aes: "target";
85
+ }, {
86
+ readonly aes: "value";
87
+ }];
88
+ readonly styleTarget: SankeyStyleTarget;
89
+ /** An annotation measures a flow's amount, or a node's total, which the stat writes in the same column. */
90
+ readonly resolveValueSource: Geom['resolveValueSource'];
91
+ /**
92
+ * Only the `sankey` stat places the flows, and it maps `node` itself. Color and alpha group by node, so they may
93
+ * only read the source or target column: a node has no value in any other, so it would go unscaled.
94
+ */
95
+ readonly validateMapping: ({ mapping, computedVariables, }: GeomMappingValidationInput) => readonly UserInputIssue[];
96
+ /** A node's center, or the middle of a flow's ribbon. Unit space runs down from the top, an anchor up. */
97
+ readonly resolveAnchorPosition: (observation: Observation) => AnchorPosition | null;
98
+ compile({ data }: GeomCompilerInput): GeomCompileResult;
99
+ }
100
+ export {};
@@ -0,0 +1,11 @@
1
+ import { StatCompileResult, StatCompilerInput, Stat } from '@graphysdk/react';
2
+ /** The aesthetics this stat maps itself, so a layer under it must not. */
3
+ export declare const SANKEY_STAT_AESTHETICS: ReadonlySet<string>;
4
+ /** The scaled aesthetics nodes paint too. Each groups by node, so it may only read the source or target column. */
5
+ export declare const SANKEY_SCALED_AESTHETICS: readonly ["color", "alpha"];
6
+ /** Lays the flows out and writes one observation per flow drawn, then one per node. */
7
+ export declare class SankeyStat extends Stat {
8
+ readonly type: "sankey";
9
+ readonly computedVariables: ReadonlySet<string>;
10
+ protected computeStat({ data, mapping, params }: StatCompilerInput): StatCompileResult;
11
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let y=require("@graphysdk/react"),S=require("d3-sankey"),O=require("react"),v=require("react/jsx-runtime");var i={kind:"sankeyKind",nodeName:"sankeyNodeName",observationKey:"sankeyObservationKey",nodeKey:"sankeyNodeKey",targetKey:"sankeyTargetKey",x0:"sankeyX0",x1:"sankeyX1",y0:"sankeyY0",y1:"sankeyY1",targetY0:"sankeyTargetY0",targetY1:"sankeyTargetY1"},k=(e,t)=>{const a=e[t];return typeof a=="number"&&Number.isFinite(a)?a:null},K=e=>{const t=e[i.observationKey],a=e[i.nodeKey],n=k(e,i.x0),o=k(e,i.x1),r=k(e,i.y0),s=k(e,i.y1);if(typeof t!="string"||typeof a!="string"||n===null||o===null||r===null||s===null)return null;switch(e[i.kind]){case"node":return{kind:"node",observation:e,key:t,nodeKey:a,x0:n,x1:o,y0:r,y1:s};case"flow":{const l=k(e,i.targetY0),u=k(e,i.targetY1),c=e[i.targetKey];return l===null||u===null||typeof c!="string"?null:{kind:"flow",observation:e,key:t,nodeKey:a,targetKey:c,x0:n,x1:o,y0:r,y1:s,targetY0:l,targetY1:u}}default:return null}},C=new WeakMap,N=e=>{const t=C.get(e);if(t!==void 0)return t;const a=[],n=[];for(const r of e){const s=K(r);s?.kind==="node"&&a.push(s),s?.kind==="flow"&&n.push(s)}const o={nodes:a,flows:n};return C.set(e,o),o},V=(e,t)=>e.nodeKey===t.nodeKey||e.targetKey===t.nodeKey,I=e=>e*e*(3-2*e),F=(e,t)=>{const a=I(t);return{top:e.y0+(e.targetY0-e.y0)*a,bottom:e.y1+(e.targetY1-e.y1)*a}},W=(e,t)=>{const a=e.x1-e.x0;if(a<=0||t.x<e.x0||t.x>e.x1)return!1;const{top:n,bottom:o}=F(e,(t.x-e.x0)/a);return t.y>=n&&t.y<=o},_=(e,t)=>{const a=u=>u*t.width,n=u=>u*t.height,o=a(e.x0),r=a(e.x1),s=a(e.x0+(e.x1-e.x0)/3),l=a(e.x0+(e.x1-e.x0)*2/3);return[`M${o},${n(e.y0)}`,`C${s},${n(e.y0)} ${l},${n(e.targetY0)} ${r},${n(e.targetY0)}`,`L${r},${n(e.targetY1)}`,`C${l},${n(e.targetY1)} ${s},${n(e.y1)} ${o},${n(e.y1)}`,"Z"].join("")},D=.018,G=.03,w={justify:S.sankeyJustify,left:S.sankeyLeft,right:S.sankeyRight,center:S.sankeyCenter},H=(e,t)=>{const a=B(e);if(a.length===0)return{nodes:[],flows:[]};const n=[...new Set(a.flatMap(r=>[r.source,r.target]))],o=(0,S.sankey)().nodeId(r=>r.key).nodeAlign(w[t]).nodeWidth(D).nodePadding(G).extent([[0,0],[1,1]])({nodes:n.map(r=>({key:r})),links:a.map(r=>({source:r.source,target:r.target,value:r.value,link:r}))});return{nodes:o.nodes.map(r=>({key:r.key,value:r.value??0,x0:p(r.x0),x1:p(r.x1),y0:p(r.y0),y1:p(r.y1)})),flows:o.links.map(r=>{const s=r.source,l=r.target,u=(r.width??0)/2;return{link:r.link,x0:p(s.x1),x1:p(l.x0),y0:p((r.y0??0)-u),y1:p((r.y0??0)+u),targetY0:p((r.y1??0)-u),targetY1:p((r.y1??0)+u)}})}},p=e=>Math.min(1,Math.max(0,e??0)),B=e=>{const t=new Map,a=(n,o)=>{const r=new Set,s=[n];for(;s.length>0;){const l=s.pop();if(l===o)return!0;r.has(l)||(r.add(l),s.push(...t.get(l)??[]))}return!1};return e.filter(n=>t.get(n.source)?.has(n.target)===!0?!0:a(n.target,n.source)?!1:(t.set(n.source,(t.get(n.source)??new Set).add(n.target)),!0))},E=new Set(["node"]),j=["color","alpha"],U=["justify","left","right","center"],X=class extends y.Stat{constructor(...e){super(...e),this.type="sankey",this.computedVariables=E}computeStat({data:e,mapping:t,params:a}){const n=Q(e,t);return n===null?{data:e,mapping:{}}:(Z(e,n),{data:ae(e,n,J(a)),mapping:{node:i.nodeName,...R(t,n)}})}},R=(e,t)=>{const a={};for(const n of j){const o=q(n,e[n],t);o!==null&&(a[n]=o)}return a},q=(e,t,{sourceVariable:a,targetVariable:n})=>{if(t===void 0)return e==="color"?i.nodeKey:null;const o=(0,y.readVariableName)(t);return o===a?i.nodeKey:o===n?i.targetKey:null},z=e=>U.includes(e),J=e=>{const t=e?.align;return z(t)?t:"justify"},L=(e,t)=>e===null?null:e instanceof Date?t.type==="year"?String(e.getUTCFullYear()):e.toISOString():String(e),Z=(e,{valueVariable:t})=>{const a=e.getType(t);if(!(e.size()===0||a==="numeric"))throw new y.UserInputError({code:"INCOMPATIBLE_TYPE",message:`'sankey' stat requires a numeric 'value' (got '${a}' for variable '${t}')`,context:{aesthetic:"value",variableName:t,variableType:a},suggestion:"Map each flow's amount to 'value', declaring the column numeric if it holds whole years."})},Q=(e,t)=>{const a=(0,y.readVariableName)(t.source),n=(0,y.readVariableName)(t.target),o=(0,y.readVariableName)(t.value);return a===null||n===null||o===null||![a,n,o].every(r=>e.hasVariable(r))?null:{sourceVariable:a,targetVariable:n,valueVariable:o}},ee=(e,{sourceVariable:t,targetVariable:a,valueVariable:n})=>{const o=e.getValues(t),r=e.getValues(a),s=e.getValues(n,{type:"numeric"}),l=e.getValueFormat(t),u=e.getValueFormat(a),c=new Map,g=new Map,d=[];return o.forEach((h,m)=>{const b=r[m]??null,f=L(h,l),x=L(b,u);if(f===null||x===null)return;g.set(f,h),g.set(x,b);const P=JSON.stringify([f,x]),Y=c.get(P)??0;c.set(P,Y+1);const A=s[m];typeof A!="number"||!Number.isFinite(A)||A<=0||d.push({source:f,target:x,value:A,row:m,occurrence:Y})}),{links:d,names:g}},$=(e,{sourceVariable:t,targetVariable:a})=>e.getType(t)===e.getType(a),te=(e,t,a,n)=>{const o=e.getType(t.sourceVariable);return $(e,t)?{type:o,values:a.map(r=>n.get(r)??null),valueFormat:e.getValueFormat(t.sourceVariable)}:{type:"categorical",values:[...a]}},re=(e,t,a,n,o)=>e===a.valueVariable?n.nodes.map(r=>r.value):e===a.sourceVariable&&$(t,a)?n.nodes.map(r=>o.get(r.key)??null):null,ae=(e,t,a)=>{const{links:n,names:o}=ee(e,t),r=H(n,a),s=r.nodes.length,l=d=>[...d,...Array.from({length:s},()=>null)],u=e.getVariableNames(),c=Object.fromEntries(u.map(d=>{const h=e.getValues(d),m=r.flows.map(({link:x})=>h[x.row]??null),b=re(d,e,t,r,o),f=b===null?l(m):[...m,...b];return[d,{type:e.getType(d),values:f,valueFormat:e.getValueFormat(d)}]})),g=[...r.flows.map(({link:d})=>d.source),...r.nodes.map(({key:d})=>d)];return new y.Dataset({...c,...ne(r),[i.nodeName]:te(e,t,g,o)})},ne=e=>{const t=e.flows.map(({link:l,...u})=>({[i.kind]:"flow",[i.observationKey]:JSON.stringify(["flow",l.source,l.target,l.occurrence]),[i.nodeKey]:l.source,[i.targetKey]:l.target,[i.x0]:u.x0,[i.x1]:u.x1,[i.y0]:u.y0,[i.y1]:u.y1,[i.targetY0]:u.targetY0,[i.targetY1]:u.targetY1})),a=e.nodes.map(l=>({[i.kind]:"node",[i.observationKey]:JSON.stringify(["node",l.key]),[i.nodeKey]:l.key,[i.targetKey]:l.key,[i.x0]:l.x0,[i.x1]:l.x1,[i.y0]:l.y0,[i.y1]:l.y1,[i.targetY0]:null,[i.targetY1]:null})),n=[...t,...a],o=l=>n.map(u=>u[l]),r=l=>({type:"categorical",values:o(l)}),s=l=>({type:"numeric",values:o(l)});return{[i.kind]:r(i.kind),[i.observationKey]:r(i.observationKey),[i.nodeKey]:r(i.nodeKey),[i.targetKey]:r(i.targetKey),[i.x0]:s(i.x0),[i.x1]:s(i.x1),[i.y0]:s(i.y0),[i.y1]:s(i.y1),[i.targetY0]:s(i.targetY0),[i.targetY1]:s(i.targetY1)}},oe=.4,se=.65,ie=12,le=3,ue={},ce={fill:"paint",fillAlpha:"unitInterval",alpha:"unitInterval",stroke:"color",strokeWidth:"pixels"},ye={fontFamily:"fontFamily",fontSize:"pixels",fontWeight:"fontWeight",textColor:"color",textOutlineColor:"color",textOutlineWidth:"pixels"},M=class extends y.Geom{constructor(...e){super(...e),this.type="sankey",this.defaultParams={align:"justify"},this.defaultStat=new X,this.aesthetics=[{kind:"data",name:"source",required:!0},{kind:"data",name:"target",required:!0},{kind:"data",name:"value",required:!0},{kind:"data",name:"node"},{kind:"visual",name:"color"},{kind:"visual",name:"alpha"}],this.identityKey={variable:i.observationKey},this.supportedPositions=["identity"],this.supportedCoordTypes=["cartesian"],this.legend={defaultPosition:"none"},this.spatialKind="render-hit-test",this.highlightStrategy="observation-rerender",this.tooltip=[{aes:"node",heading:!0},{aes:"target"},{aes:"value"}],this.styleTarget=(0,y.defineGeomStyleTarget)({observation:{vocabulary:ue,aesthetics:{fill:"color",alpha:"alpha"},rest:{fill:(0,y.token)("geom"),fillAlpha:oe},hovered:{fillAlpha:se}},parts:{node:{vocabulary:ce,aesthetics:{fill:"color",alpha:"alpha"},rest:{fill:(0,y.token)("geom"),fillAlpha:1,alpha:1,stroke:(0,y.token)("graphBackground"),strokeWidth:0}},label:{vocabulary:ye,rest:{fontSize:ie,fontWeight:500,textColor:(0,y.token)("textPrimary"),textOutlineColor:(0,y.token)("graphBackground"),textOutlineWidth:le}}}}),this.resolveValueSource=t=>t.value??null,this.validateMapping=({mapping:t,computedVariables:a})=>{if(!a.has("node"))return[{code:"MISSING_STAT_OUTPUT",message:"sankey geom lays its flows out in the 'sankey' stat; remove the layer's stat"}];const n=[...E].filter(s=>t[s]!==void 0).map(s=>({code:"CONFLICTING_STAT_MAPPING",message:`sankey stat computes '${s}' from 'source' and 'target'; remove its mapping`})),o=[(0,y.readVariableName)(t.source),(0,y.readVariableName)(t.target)],r=j.flatMap(s=>{const l=(0,y.readVariableName)(t[s]);return l===null||o.includes(l)?[]:[{code:"UNSUPPORTED_MAPPING",message:`sankey geom scales '${s}' by node; map it to the source or target column rather than '${l}'`}]});return[...n,...r]},this.resolveAnchorPosition=t=>{const a=K(t);if(a===null)return null;if(a.kind==="node")return{x:(a.x0+a.x1)/2,y:1-(a.y0+a.y1)/2};const{top:n,bottom:o}=F(a,.5);return{x:(a.x0+a.x1)/2,y:1-(n+o)/2}}}compile({data:e}){return{data:e,mapping:{}}}},de=({layer:e,panelRect:t})=>{const{nodes:a,flows:n}=N(e.data),o=s=>s*t.width,r=s=>s*t.height;return[...n.map(s=>({kind:"region",pathData:_(s,t)})),...a.map(s=>({kind:"region",pathData:`M${o(s.x0)},${r(s.y0)}H${o(s.x1)}V${r(s.y1)}H${o(s.x0)}Z`}))]},ge=({flows:e,size:t,source:a,styleReaders:n,isHovered:o=!1,shouldAnimateTransitions:r=!1})=>{const s=o?"hovered":void 0,l=(0,O.useMemo)(()=>e.map(g=>n.get("fill",g.observation,s)??"transparent"),[e,n,s]),{fillFor:u,defs:c}=(0,y.usePaintDefs)({paints:l});return(0,v.jsxs)(v.Fragment,{children:[c,e.map((g,d)=>{const h=l[d],m=n.get("stroke",g.observation,s);return(0,v.jsx)(y.GeomPath,{"data-testid":"sankey-flow",d:_(g,t),source:a,fill:h===void 0?void 0:u(h),opacity:n.get("alpha",g.observation,s),fillOpacity:n.get("fillAlpha",g.observation,s),stroke:m===void 0?void 0:(0,y.toPaintColor)(m),strokeOpacity:n.get("strokeAlpha",g.observation,s),shouldAnimateTransitions:r},g.key)})]})},ve=6,he=e=>{const t=(e.y0+e.y1)/2;return(e.x0+e.x1)/2<.5?{x:e.x1,y:t,offsetX:ve,textAnchor:"start"}:{x:e.x0,y:t,offsetX:-6,textAnchor:"end"}},me=({nodes:e,valueFormat:t,styleReaders:a,isHovered:n=!1})=>{const o=n?"hovered":void 0,r=a.part("label"),s=(0,y.useGraphLocale)(),l=(0,y.useSceneSelector)(c=>c.config.numberFormat),u=(0,O.useMemo)(()=>(0,y.createValueFormatter)({valueFormat:t,locale:s,numberFormat:l}),[t,s,l]);return(0,v.jsx)("g",{pointerEvents:"none",children:e.map(c=>{const g=c.observation[i.nodeName];if(g==null)return null;const d=he(c),h=r?.get("textColor",c.observation,o),m=r?.get("textOutlineColor",c.observation,o);return(0,v.jsx)("text",{"data-testid":"sankey-label",x:(0,y.toPercent)(d.x),y:(0,y.toPercent)(d.y),dx:d.offsetX,textAnchor:d.textAnchor,dominantBaseline:"central",fontFamily:r?.get("fontFamily",c.observation,o),fontSize:r?.get("fontSize",c.observation,o),fontWeight:r?.get("fontWeight",c.observation,o),fill:h===void 0?void 0:(0,y.toPaintColor)(h),stroke:m===void 0?void 0:(0,y.toPaintColor)(m),strokeWidth:r?.get("textOutlineWidth",c.observation,o),strokeLinejoin:"round",paintOrder:"stroke",children:u(g,c.observation)},c.key)})})},pe=({nodes:e,styleReaders:t,isHovered:a=!1,shouldAnimateTransitions:n=!1})=>{const o=a?"hovered":void 0,r=t.part("node"),s=(0,O.useMemo)(()=>e.map(c=>r?.get("fill",c.observation,o)??"transparent"),[e,r,o]),{fillFor:l,defs:u}=(0,y.usePaintDefs)({paints:s});return(0,v.jsxs)(v.Fragment,{children:[u,e.map((c,g)=>{const d=s[g],h=r?.get("stroke",c.observation,o);return(0,v.jsx)(y.GeomRect,{"data-testid":"sankey-node",x:(0,y.toPercent)(c.x0),y:(0,y.toPercent)(c.y0),width:(0,y.toPercent)(c.x1-c.x0),height:(0,y.toPercent)(c.y1-c.y0),fill:d===void 0?void 0:l(d),opacity:r?.get("alpha",c.observation,o),fillOpacity:r?.get("fillAlpha",c.observation,o),stroke:h===void 0?void 0:(0,y.toPaintColor)(h),strokeWidth:r?.get("strokeWidth",c.observation,o),shouldAnimateTransitions:n},c.key)})]})},fe={type:"text"},T=({observations:e,layer:t,size:a,styleReaders:n,isHovered:o=!1,shouldAnimateTransitions:r=!1})=>(0,v.jsxs)(v.Fragment,{children:[(0,v.jsx)(ge,{flows:e.flows,size:a,source:t,styleReaders:n,isHovered:o,shouldAnimateTransitions:r}),(0,v.jsx)(pe,{nodes:e.nodes,styleReaders:n,isHovered:o,shouldAnimateTransitions:r}),(0,v.jsx)(me,{nodes:e.nodes,valueFormat:t.data.hasVariable(i.nodeName)?t.data.getValueFormat(i.nodeName):fe,styleReaders:n,isHovered:o})]}),xe=({layer:e,size:t,styleReaders:a})=>(0,v.jsx)(T,{observations:N(e.data),layer:e,size:t,styleReaders:a}),ke=({layer:e})=>{const{nodes:t,flows:a}=N(e.data),n=[...t].reverse(),o=[...a].reverse();return r=>{const s=n.find(u=>r.x>=u.x0&&r.x<=u.x1&&r.y>=u.y0&&r.y<=u.y1);if(s!==void 0)return{key:s.key};const l=o.find(u=>W(u,r));return l===void 0?null:{key:l.key}}},be=({layer:e,primary:t,size:a,styleReaders:n})=>{const o=N(e.data),r=t.observation;return(0,v.jsx)("g",{"data-testid":"hovered-observations",children:(0,v.jsx)(T,{observations:(0,O.useMemo)(()=>Se(o,r),[o,r]),layer:e,size:a,styleReaders:n,isHovered:!0})})},Se=(e,t)=>{const a=K(t);if(a===null)return{nodes:[],flows:[]};if(a.kind==="node"){const n=e.flows.filter(r=>V(r,a)),o=new Set(n.flatMap(r=>[r.nodeKey,r.targetKey]));return{nodes:e.nodes.filter(r=>o.has(r.nodeKey)),flows:n}}return{nodes:e.nodes.filter(n=>V(a,n)),flows:[a]}},Ne=({layer:e,size:t,styleReaders:a,shouldAnimateTransitions:n})=>(0,v.jsx)(T,{observations:N(e.data),layer:e,size:t,styleReaders:a,shouldAnimateTransitions:n}),Ae=(0,y.defineGeomRenderer)(new M,{coord:"cartesian",swatchShape:"square",render:({layer:e,panelRect:t,styleReaders:a,shouldAnimateTransitions:n})=>(0,v.jsx)(Ne,{layer:e,size:t,styleReaders:a,shouldAnimateTransitions:n}),renderHighlight:({layer:e,panelRect:t,styleReaders:a})=>(0,v.jsx)(xe,{layer:e,size:t,styleReaders:a}),renderHover:({layer:e,primary:t,panelRect:a,styleReaders:n})=>(0,v.jsx)(be,{layer:e,primary:t,size:a,styleReaders:n}),renderHoverCompanions:()=>null,hitTest:ke,getEditOutlineShapes:de});exports.SankeyGeom=M;exports.sankey=Ae;
@@ -0,0 +1,2 @@
1
+ export { SankeyGeom } from './engine/sankey.geom';
2
+ export { sankey } from './renderer/sankey';