* sankey

* sanki2

* numine data example

* remove showcase

* address PR review comments for sankey chart

- move @types/d3-sankey to devDependencies
- use targeted d3-selection/d3-transition imports in SankeyLink
- add explicit return type to useSankeyController
- extract LABEL_LINE_HEIGHT/LABEL_LINE_GAP module-level constants
- add readonly modifiers to SankeyNodeDatum, SankeyLinkDatum, SankeyData
- add comment explaining d3-sankey runtime type assertions in SankeyNode

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* address PR review comments for sankey chart

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* review fixes

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Igor Stuev
2026-03-03 19:45:06 +03:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 18bc3e0806
commit d8bc35d94c
9 changed files with 538 additions and 0 deletions
+2
View File
@@ -93,6 +93,7 @@
"crypto-js": "^4.2.0",
"cspell": "9.6.4",
"d3": "^7.6.1",
"d3-sankey": "^0.12.3",
"dappscout-iframe": "0.4.0",
"dayjs": "^1.11.5",
"dom-to-image": "^2.6.0",
@@ -152,6 +153,7 @@
"@types/crypto-js": "^4.1.1",
"@types/csp-dev": "^1.0.0",
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.5",
"@types/dom-to-image": "^2.6.4",
"@types/js-cookie": "^3.0.2",
"@types/mixpanel-browser": "^2.38.1",
@@ -0,0 +1,161 @@
import { useToken } from '@chakra-ui/react';
import React from 'react';
import type { ChartMargin } from '../types';
import type { SankeyData, SankeyNodeExtended } from './types';
import { useColorModeValue } from 'toolkit/chakra/color-mode';
import {
DEFAULT_SANKEY_LINK_HOVER_OPACITY,
DEFAULT_SANKEY_LINK_OPACITY,
SANKEY_NODE_COLOR_TOKENS_DARK,
SANKEY_NODE_COLOR_TOKENS_LIGHT,
} from './constants';
import { SankeyLink } from './parts/SankeyLink';
import { SankeyNode } from './parts/SankeyNode';
import { useSankeyController } from './useSankeyController';
export type SankeyLinkColorMode = 'source' | 'target';
export interface SankeyChartProps {
data: SankeyData;
margin?: ChartMargin;
nodeWidth?: number;
nodePadding?: number;
linkOpacity?: number;
linkHoverOpacity?: number;
linkColorMode?: SankeyLinkColorMode;
colors?: ReadonlyArray<string>;
valueFormatter?: (value: number) => string;
}
const DEFAULT_CHART_MARGIN: ChartMargin = { top: 40, right: 8, bottom: 8, left: 8 };
const LABEL_Y_GAP = 4;
const LABEL_LINE_HEIGHT = 16;
const LABEL_LINE_GAP = 2;
export const SankeyChart = React.memo(({
data,
margin: marginProps,
nodeWidth,
nodePadding,
linkOpacity = DEFAULT_SANKEY_LINK_OPACITY,
linkHoverOpacity = DEFAULT_SANKEY_LINK_HOVER_OPACITY,
linkColorMode = 'source',
colors: colorsProp,
valueFormatter,
}: SankeyChartProps) => {
const margin: ChartMargin = React.useMemo(() => ({ ...DEFAULT_CHART_MARGIN, ...marginProps }), [ marginProps ]);
const colorTokens = useColorModeValue(SANKEY_NODE_COLOR_TOKENS_LIGHT, SANKEY_NODE_COLOR_TOKENS_DARK);
const resolvedDefaultColors = useToken('colors', colorTokens);
const colors = colorsProp && colorsProp.length > 0 ? colorsProp : resolvedDefaultColors;
const [ labelColor ] = useToken('colors', 'text.secondary');
const [ labelFontSize ] = useToken('fontSizes', 'xs');
const {
ref,
rect,
innerWidth,
nodes,
links,
linkPathGenerator,
} = useSankeyController({
data,
margin,
nodeWidth,
nodePadding,
});
const nodeColorMap = React.useMemo(() => {
const map = new Map<string, string>();
nodes.forEach((node, index) => {
map.set(node.id, colors[index % colors.length]);
});
return map;
}, [ nodes, colors ]);
const getNodeColor = React.useCallback((node: SankeyNodeExtended) => {
return nodeColorMap.get(node.id) || colors[0];
}, [ nodeColorMap, colors ]);
const formatValue = React.useCallback((value: number | undefined) => {
if (value === undefined) {
return '';
}
return valueFormatter ? valueFormatter(value) : String(value);
}, [ valueFormatter ]);
return (
<svg width="100%" height="100%" ref={ ref } opacity={ rect ? 1 : 0 }>
<g transform={ `translate(${ margin.left || 0 },${ margin.top || 0 })` }>
{ links.map((link, index) => (
<SankeyLink
key={ `link-${ index }` }
link={ link }
color={ getNodeColor(linkColorMode === 'target' ? link.target : link.source) }
sourceColor={ getNodeColor(link.source) }
targetColor={ getNodeColor(link.target) }
gradientIdSuffix={ index }
opacity={ linkOpacity }
hoverOpacity={ linkHoverOpacity }
pathGenerator={ linkPathGenerator }
/>
)) }
{ nodes.map((node) => (
<SankeyNode
key={ node.id }
node={ node }
color={ getNodeColor(node) }
/>
)) }
{ nodes.map((node) => {
const { x0 = 0, x1 = 0, y0 = 0 } = node;
const isSource = (x0 || 0) <= innerWidth / 2;
const nodeId = node.id;
const nodeName = node.name;
const valueStr = node.value !== undefined ? formatValue(node.value) : '';
const blockHeight = LABEL_LINE_HEIGHT + (valueStr ? LABEL_LINE_GAP + LABEL_LINE_HEIGHT : 0);
const blockTop = (y0 || 0) - LABEL_Y_GAP - blockHeight;
const nameCenterY = blockTop + LABEL_LINE_HEIGHT / 2;
const valueCenterY = blockTop + LABEL_LINE_HEIGHT + LABEL_LINE_GAP + LABEL_LINE_HEIGHT / 2;
const labelX = isSource ? (x0 || 0) : (x1 || 0);
const anchor = isSource ? 'start' : 'end';
return (
<g key={ `label-${ nodeId }` }>
<text
x={ labelX }
y={ nameCenterY }
textAnchor={ anchor }
dominantBaseline="central"
fill={ labelColor }
style={{ fontSize: labelFontSize, fontWeight: 600 }}
>
{ nodeName }
</text>
{ valueStr && (
<text
x={ labelX }
y={ valueCenterY }
textAnchor={ anchor }
dominantBaseline="central"
fill={ labelColor }
style={{ fontSize: labelFontSize, fontWeight: 400 }}
>
{ valueStr }
</text>
) }
</g>
);
}) }
</g>
</svg>
);
});
@@ -0,0 +1,38 @@
/**
* Sankey node color tokens from Figma (Universal Bridge Indexer).
* Light theme: Chakra 300 palette. Resolved via useToken in the chart.
* @see https://www.figma.com/design/3mGpZEE65cSuLnhtJBfKkO?node-id=6867-20752
*/
export const SANKEY_NODE_COLOR_TOKENS_LIGHT: Array<string> = [
'blue.300',
'purple.300',
'pink.300',
'green.300',
'teal.300',
'cyan.300',
'orange.300',
'yellow.300',
'red.300',
];
/**
* Sankey node color tokens from Figma (Universal Bridge Indexer).
* Dark theme: Chakra 700 palette. Resolved via useToken in the chart.
* @see https://www.figma.com/design/3mGpZEE65cSuLnhtJBfKkO?node-id=6867-21363
*/
export const SANKEY_NODE_COLOR_TOKENS_DARK: Array<string> = [
'blue.700',
'purple.700',
'pink.700',
'green.700',
'teal.700',
'cyan.700',
'orange.700',
'yellow.700',
'red.700',
];
export const DEFAULT_SANKEY_NODE_WIDTH = 16;
export const DEFAULT_SANKEY_NODE_PADDING = 40;
export const DEFAULT_SANKEY_LINK_OPACITY = 0.35;
export const DEFAULT_SANKEY_LINK_HOVER_OPACITY = 0.6;
@@ -0,0 +1,6 @@
export * from './SankeyChart';
export * from './parts/SankeyNode';
export * from './parts/SankeyLink';
export * from './useSankeyController';
export * from './types';
export * from './constants';
@@ -0,0 +1,109 @@
import { select } from 'd3-selection';
import 'd3-transition';
import React from 'react';
import type { SankeyLinkExtended } from '../types';
export interface SankeyLinkProps {
link: SankeyLinkExtended;
/** Single color when gradient is not used */
color?: string;
/** When set with targetColor, link stroke is a gradient from source to target */
sourceColor?: string;
targetColor?: string;
/** Unique suffix for gradient id (e.g. link index) when multiple links share same source/target */
gradientIdSuffix?: string | number;
opacity: number;
hoverOpacity: number;
pathGenerator: (link: SankeyLinkExtended, ...args: Array<any>) => string | null;
onMouseEnter?: (link: SankeyLinkExtended, event: React.MouseEvent) => void;
onMouseLeave?: () => void;
}
export const SankeyLink = React.memo(({
link,
color,
sourceColor,
targetColor,
gradientIdSuffix,
opacity,
hoverOpacity,
pathGenerator,
onMouseEnter,
onMouseLeave,
}: SankeyLinkProps) => {
const ref = React.useRef<SVGPathElement>(null);
const handleMouseEnter = React.useCallback((event: React.MouseEvent) => {
if (ref.current) {
select(ref.current).transition().duration(200).attr('stroke-opacity', hoverOpacity);
}
onMouseEnter?.(link, event);
}, [ link, hoverOpacity, onMouseEnter ]);
const handleMouseLeave = React.useCallback(() => {
if (ref.current) {
select(ref.current).transition().duration(200).attr('stroke-opacity', opacity);
}
onMouseLeave?.();
}, [ opacity, onMouseLeave ]);
const source = link.source;
const target = link.target;
const d = pathGenerator(link);
if (!d) {
return null;
}
const sanitizeSvgId = (value: string | number): string => String(value).replace(/[^\w-]/g, '_');
const useGradient = sourceColor != null && targetColor != null && sourceColor !== targetColor;
const gradientId = useGradient ?
`sankey-link-${ sanitizeSvgId(source.id) }-${ sanitizeSvgId(target.id) }-${ sanitizeSvgId(gradientIdSuffix ?? '') }` :
undefined;
const strokeColor = useGradient && gradientId ?
`url(#${ gradientId })` :
color;
const sourceX = source.x1 ?? 0;
const sourceY = ((source.y0 ?? 0) + (source.y1 ?? 0)) / 2;
const targetX = target.x0 ?? 0;
const targetY = ((target.y0 ?? 0) + (target.y1 ?? 0)) / 2;
return (
<>
{ useGradient && gradientId && (
<defs>
<linearGradient
id={ gradientId }
gradientUnits="userSpaceOnUse"
x1={ sourceX }
y1={ sourceY }
x2={ targetX }
y2={ targetY }
>
<stop offset="0%" stopColor={ sourceColor }/>
<stop offset="100%" stopColor={ targetColor }/>
</linearGradient>
</defs>
) }
<path
ref={ ref }
d={ d }
fill="none"
stroke={ strokeColor }
strokeOpacity={ opacity }
strokeWidth={ Math.max(1, link.width || 0) }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
>
<title>{ `${ source.name } → ${ target.name }: ${ link.value }` }</title>
</path>
</>
);
});
@@ -0,0 +1,74 @@
import React from 'react';
import type { SankeyNodeExtended } from '../types';
const OUTER_CORNER_RADIUS = 4;
export interface SankeyNodeProps {
node: SankeyNodeExtended;
color: string;
onMouseEnter?: (node: SankeyNodeExtended, event: React.MouseEvent) => void;
onMouseLeave?: () => void;
}
export const SankeyNode = React.memo(({ node, color, onMouseEnter, onMouseLeave }: SankeyNodeProps) => {
const { x0 = 0, x1 = 0, y0 = 0, y1 = 0 } = node;
const width = x1 - x0;
const height = y1 - y0;
const handleMouseEnter = React.useCallback((event: React.MouseEvent) => {
onMouseEnter?.(node, event);
}, [ node, onMouseEnter ]);
const hasIncoming = node.targetLinks?.length;
const hasOutgoing = node.sourceLinks?.length;
const isMiddle = Boolean(hasIncoming && hasOutgoing);
const path = React.useMemo(() => {
if (width <= 0 || height <= 0 || isMiddle) {
return null;
}
const r = Math.min(OUTER_CORNER_RADIUS, width / 2, height / 2);
if (hasIncoming) {
return (
`M ${ x0 },${ y0 } L ${ x1 - r },${ y0 } Q ${ x1 },${ y0 } ${ x1 },${ y0 + r } ` +
`L ${ x1 },${ y1 - r } Q ${ x1 },${ y1 } ${ x1 - r },${ y1 } L ${ x0 },${ y1 } L ${ x0 },${ y0 } Z`
);
}
return (
`M ${ x1 },${ y0 } L ${ x0 + r },${ y0 } Q ${ x0 },${ y0 } ${ x0 },${ y0 + r } ` +
`L ${ x0 },${ y1 - r } Q ${ x0 },${ y1 } ${ x0 + r },${ y1 } L ${ x1 },${ y1 } L ${ x1 },${ y0 } Z`
);
}, [ x0, x1, y0, y1, width, height, isMiddle, hasIncoming ]);
if (!path) {
if (isMiddle) {
return (
<rect
x={ x0 }
y={ y0 }
width={ width }
height={ height }
fill={ color }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ onMouseLeave }
>
<title>{ `${ node.name }: ${ node.value }` }</title>
</rect>
);
}
return null;
}
return (
<path
d={ path }
fill={ color }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ onMouseLeave }
>
<title>{ `${ node.name }: ${ node.value }` }</title>
</path>
);
});
+26
View File
@@ -0,0 +1,26 @@
import type { SankeyNode, SankeyLink } from 'd3-sankey';
export interface SankeyNodeDatum {
readonly id: string;
readonly name: string;
}
export interface SankeyLinkDatum {
readonly source: string;
readonly target: string;
readonly value: number;
}
export interface SankeyData {
readonly nodes: ReadonlyArray<SankeyNodeDatum>;
readonly links: ReadonlyArray<SankeyLinkDatum>;
}
// After d3-sankey layout, nodes are the input datum merged with the layout properties.
export type SankeyNodeExtended = SankeyNodeDatum & SankeyNode<SankeyNodeDatum, SankeyLinkDatum>;
// After d3-sankey layout, source/target are always resolved node objects, not ids.
export type SankeyLinkExtended = Omit<SankeyLink<SankeyNodeDatum, SankeyLinkDatum>, 'source' | 'target'> & {
source: SankeyNodeExtended;
target: SankeyNodeExtended;
};
@@ -0,0 +1,71 @@
import { sankey, sankeyLinkHorizontal, sankeyJustify } from 'd3-sankey';
import React from 'react';
import type { ChartMargin } from '../types';
import type { SankeyData, SankeyLinkExtended, SankeyNodeExtended } from './types';
import { calculateInnerSize } from 'toolkit/components/charts/utils/calculateInnerSize';
import { useClientRect } from 'toolkit/hooks/useClientRect';
import { DEFAULT_SANKEY_NODE_PADDING, DEFAULT_SANKEY_NODE_WIDTH } from './constants';
interface Props {
data: SankeyData;
margin?: ChartMargin;
nodeWidth?: number;
nodePadding?: number;
}
interface SankeyLayout {
nodes: Array<SankeyNodeExtended>;
links: Array<SankeyLinkExtended>;
}
interface UseSankeyControllerResult {
ref: React.Ref<SVGSVGElement> | undefined;
readonly rect: DOMRect | null;
readonly innerWidth: number;
readonly innerHeight: number;
readonly nodes: Array<SankeyNodeExtended>;
readonly links: Array<SankeyLinkExtended>;
readonly linkPathGenerator: (link: SankeyLinkExtended) => string | null;
}
export function useSankeyController({ data, margin, nodeWidth, nodePadding }: Props): UseSankeyControllerResult {
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, margin);
const layout: SankeyLayout = React.useMemo(() => {
if (innerWidth <= 0 || innerHeight <= 0) {
return { nodes: [], links: [] };
}
const sankeyGenerator = sankey<SankeyData['nodes'][number], SankeyData['links'][number]>()
.nodeId((d) => d.id)
.nodeWidth(nodeWidth ?? DEFAULT_SANKEY_NODE_WIDTH)
.nodePadding(nodePadding ?? DEFAULT_SANKEY_NODE_PADDING)
.nodeAlign(sankeyJustify)
.extent([ [ 0, 0 ], [ innerWidth, innerHeight ] ]);
const { nodes, links } = sankeyGenerator({
nodes: data.nodes.map((d) => ({ ...d })),
links: data.links.map((d) => ({ ...d })),
});
// After layout, source/target on each link are resolved node objects, not ids.
return { nodes: nodes as Array<SankeyNodeExtended>, links: links as unknown as Array<SankeyLinkExtended> };
}, [ data, innerWidth, innerHeight, nodeWidth, nodePadding ]);
const linkPathGenerator: (link: SankeyLinkExtended) => string | null = React.useMemo(() => sankeyLinkHorizontal(), []);
return React.useMemo(() => ({
ref,
rect,
innerWidth,
innerHeight,
nodes: layout.nodes,
links: layout.links,
linkPathGenerator,
}), [ ref, rect, innerWidth, innerHeight, layout, linkPathGenerator ]);
}
+51
View File
@@ -9655,6 +9655,11 @@
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b"
integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==
"@types/d3-path@^1":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.11.tgz#45420fee2d93387083b34eae4fe6d996edf482bc"
integrity sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==
"@types/d3-polygon@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93"
@@ -9670,6 +9675,13 @@
resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953"
integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==
"@types/d3-sankey@^0.12.5":
version "0.12.5"
resolved "https://registry.yarnpkg.com/@types/d3-sankey/-/d3-sankey-0.12.5.tgz#9b79ff5768250cfd8c6340b167c409a0374f40bd"
integrity sha512-/3RZSew0cLAtzGQ+C89hq/Rp3H20QJuVRSqFy6RKLe7E0B8kd2iOS1oBsodrgds4PcNVpqWhdUEng/SHvBcJ6Q==
dependencies:
"@types/d3-shape" "^1"
"@types/d3-scale-chromatic@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954"
@@ -9694,6 +9706,13 @@
dependencies:
"@types/d3-path" "*"
"@types/d3-shape@^1":
version "1.3.12"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.12.tgz#8f2f9f7a12e631ce6700d6d55b84795ce2c8b259"
integrity sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==
dependencies:
"@types/d3-path" "^1"
"@types/d3-time-format@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946"
@@ -13943,6 +13962,13 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
"d3-array@1 - 2":
version "2.12.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
dependencies:
internmap "^1.0.0"
"d3-array@2 - 3", "[email protected] - 3", "[email protected] - 3", d3-array@3, d3-array@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.0.tgz#15bf96cd9b7333e02eb8de8053d78962eafcff14"
@@ -14059,6 +14085,11 @@ d3-hierarchy@3:
dependencies:
d3-color "1 - 3"
d3-path@1:
version "1.0.9"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
"d3-path@1 - 3", d3-path@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e"
@@ -14079,6 +14110,14 @@ d3-random@3:
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
d3-sankey@^0.12.3:
version "0.12.3"
resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d"
integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==
dependencies:
d3-array "1 - 2"
d3-shape "^1.2.0"
d3-scale-chromatic@3:
version "3.0.0"
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
@@ -14110,6 +14149,13 @@ d3-shape@3:
dependencies:
d3-path "1 - 3"
d3-shape@^1.2.0:
version "1.3.7"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
dependencies:
d3-path "1"
"d3-time-format@2 - 4", d3-time-format@4:
version "4.1.0"
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
@@ -17084,6 +17130,11 @@ internal-slot@^1.1.0:
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"