UNPKG

@graphique/graphique

Version:

A data visualization system for React based on the Grammar of Graphics.

1 lines 167 kB
{"version":3,"sources":["../src/index.ts","../src/gg/GG.tsx","../src/util/dates.ts","../src/util/autoScale.ts","../src/util/scaleDefaults.ts","../src/util/defineGroupAccessor.ts","../src/util/debounce.ts","../src/util/directlyStyleNodes.ts","../src/util/getYAlongPath.ts","../src/util/elongate.ts","../src/util/widen.ts","../src/util/EventArea.tsx","../src/atoms/labels.ts","../src/atoms/theme.ts","../src/atoms/tooltip.ts","../src/atoms/scales/x.ts","../src/atoms/scales/y.ts","../src/atoms/scales/fill.ts","../src/atoms/scales/stroke.ts","../src/atoms/scales/dashArray.ts","../src/atoms/scales/radius.ts","../src/atoms/zoom.ts","../src/gg/types/Brush.ts","../src/gg/types/Legend.ts","../src/gg/GGBase.tsx","../src/util/flattenChildren.tsx","../src/gg/axes/XAxis.tsx","../src/gg/axes/YAxis.tsx","../src/gg/zoom/zoom.tsx","../src/gg/zoom/useUnZoom.ts","../src/gg/zoom/ZoomOutButton/index.tsx","../src/gg/zoom/ZoomOutButton/Portal.tsx","../src/gg/zoom/ZoomOutButton/styles.ts","../src/util/brushing/index.tsx","../src/util/generateID.ts","../src/util/formatMissing.ts","../src/util/nodeToString.ts","../src/util/index.ts","../src/gg/labels/index.tsx","../src/gg/theme/index.tsx","../src/gg/tooltip/Tooltip.tsx","../src/gg/tooltip/YTooltip.tsx","../src/gg/tooltip/TooltipPortals.tsx","../src/gg/tooltip/XTooltip.tsx","../src/gg/tooltip/TooltipContainer.tsx","../src/gg/scales/x.tsx","../src/gg/scales/y.tsx","../src/gg/scales/fill.tsx","../src/gg/scales/stroke.tsx","../src/gg/scales/strokeDasharray.tsx","../src/gg/scales/size.tsx"],"sourcesContent":["export * from \"./gg\"\nexport * from \"./util\"\nexport * from \"./atoms\"\n","import React, {\n useMemo,\n useRef,\n useLayoutEffect,\n useEffect,\n useState,\n} from 'react'\nimport { Provider } from 'jotai'\nimport { generateID } from '../util'\nimport { GGBase } from './GGBase'\nimport type { RootGGProps } from './types/GG'\n\n/**\n * **The top-level component and starting point for creating Graphique visualizations**.\n *\n * Pass in data, map data properties to visual properties ([`aes`](https://graphique.dev/docs/graphique/gg#Aes)), and fill with child Geoms\n * to start visualizing data. Configure and customize with a friendly, component-based API.\n *\n * @param data - the data used to create the base, an array of objects\n * @param aes - an object of accessor methods to map data characteristics to visual characteristics\n * @param width - the width of the visualization area in pixels\n * @param height - the height of the visualization area in pixels\n * @param margin - an object specifying the margins surrounding the visualization area\n * @param isContainerWidth - when true, the visualization will fill its parent container's width\n * @param children - elements used to specify and configure the visualization (e.g. Geoms, Scales, Labels, Theme, etc.)\n *\n * @returns {React.JSX.Element} A React element that renders your visualization!\n */\nexport const GG = <Datum,>({ children, ...props }: RootGGProps<Datum>) => {\n const { data, aes, width, height, margin, isContainerWidth } = { ...props }\n const ggRef = useRef<HTMLDivElement>(null)\n\n const [ggWidth, setGGWidth] = useState(\n isContainerWidth ? ggRef.current?.clientWidth : width,\n )\n\n useLayoutEffect(() => {\n if (isContainerWidth) setGGWidth(ggRef.current?.clientWidth)\n }, [isContainerWidth])\n\n useEffect(() => {\n const observer = new ResizeObserver((entries) => {\n const rect = entries[0].contentRect\n if (isContainerWidth) setGGWidth(rect.width)\n })\n if (ggRef.current && isContainerWidth) observer.observe(ggRef.current)\n\n return () => {\n if (ggRef.current && isContainerWidth) observer.unobserve(ggRef.current)\n }\n }, [isContainerWidth])\n\n const id = useMemo(() => generateID(), [])\n\n return (\n <div ref={ggRef}>\n <Provider>\n <GGBase\n data={data.map((d: Datum, i) => ({\n ...d,\n gg_gen_index: i,\n }))}\n aes={aes}\n width={ggWidth}\n height={height}\n margin={margin}\n id={id}\n >\n {children}\n </GGBase>\n </Provider>\n </div>\n )\n}\n","import { timeParse, timeFormat } from 'd3-time-format'\n\nexport const parseDate = (dateString: string, specifier = '%Y-%m-%d') =>\n timeParse(specifier)(dateString)\n\nexport const isDate = (date: any) =>\n Object.prototype.toString.call(date) === '[object Date]'\n\nexport const formatMonth = (v: Date, monthOnly = false): string => {\n let monthFormat\n if (monthOnly) {\n monthFormat =\n v.getMonth() === 0 ? timeFormat('%b %Y')(v) : timeFormat('%b')(v)\n } else {\n monthFormat = timeFormat('%b %Y')(v)\n }\n return monthFormat\n}\n\nexport const formatDate = (v: Date, format = '%b %d, %Y'): string =>\n timeFormat(format)(v)\n","import {\n scaleLinear,\n scaleTime,\n scaleBand,\n scaleOrdinal,\n scaleSequential,\n} from 'd3-scale'\nimport { extent } from 'd3-array'\nimport type { XYScaleProps, VisualEncodingProps } from '../atoms/scales/types'\nimport type {\n DataValue,\n XYScaleTypes,\n VisualEncodingTypes,\n GGProps,\n} from '../gg'\nimport {\n defaultScheme,\n defaultInterpolator,\n defaultDasharrays,\n createSequentialScheme,\n} from './scaleDefaults'\nimport { defineGroupAccessor } from './defineGroupAccessor'\nimport { isDate } from './dates'\n\nexport interface IScale<Datum> {\n xScale: XYScaleTypes\n yScale: XYScaleTypes\n fillScale?: VisualEncodingTypes\n strokeScale?: VisualEncodingTypes\n strokeDasharrayScale?: VisualEncodingTypes\n groupAccessor: DataValue<Datum> | undefined\n groups?: string[]\n}\n\nexport interface AutoScale<Datum> extends GGProps<Datum> {\n scalesState: {\n x: XYScaleProps\n y: XYScaleProps\n hasZeroXBaseLine: boolean\n hasZeroYBaseLine: boolean\n geomGroupAccessors: DataValue<Datum>[]\n y0Aes?: DataValue<Datum>\n y1Aes?: DataValue<Datum>\n geomAesYs: DataValue<Datum>[]\n geomAesStrokes: DataValue<Datum>[]\n geomAesStrokeDasharrays: DataValue<Datum>[]\n geomAesFills: DataValue<Datum>[]\n fill?: VisualEncodingProps\n stroke?: VisualEncodingProps\n strokeDasharray?: VisualEncodingProps\n }\n copiedData: Datum[]\n shouldExcludeMissingXYFromDomains?: boolean\n}\n\nexport const autoScale = <Datum>({\n scalesState,\n data,\n copiedData,\n aes,\n width = 500,\n height = 450,\n margin: suppliedMargin,\n shouldExcludeMissingXYFromDomains,\n}: AutoScale<Datum>): IScale<Datum> => {\n const margin = {\n top: 10,\n right: 20,\n bottom: 10,\n left: 30,\n ...suppliedMargin,\n }\n\n const {\n x: xScaleState,\n y: yScaleState,\n fill: fillScaleState,\n stroke: strokeScaleState,\n strokeDasharray: strokeDasharrayState,\n hasZeroXBaseLine,\n hasZeroYBaseLine,\n geomGroupAccessors,\n y0Aes,\n y1Aes,\n geomAesYs,\n geomAesStrokes,\n geomAesFills,\n geomAesStrokeDasharrays,\n } = scalesState\n const {\n domain: xScaleDomain,\n type: xScaleType,\n reverse: reverseX,\n } = xScaleState || {}\n const {\n domain: yScaleDomain,\n type: yScaleType,\n reverse: reverseY,\n } = yScaleState || {}\n const {\n domain: fillScaleDomain,\n type: fillScaleType,\n values: fillScaleColors,\n reverse: fillScaleReverse,\n } = fillScaleState || {}\n const {\n domain: strokeScaleDomain,\n type: strokeScaleType,\n values: strokeScaleColors,\n reverse: strokeScaleReverse,\n } = strokeScaleState || {}\n const { domain: strokeDasharrayDomain, values: strokeDasharrays } =\n strokeDasharrayState || {}\n\n // used for maintaining the member order (domain) in categorical axes\n const sortDomain = (a: string, b: string, initialDomain: string[]) =>\n initialDomain.indexOf(a) - initialDomain.indexOf(b)\n\n const geomGroupAccessor = geomGroupAccessors.length\n ? geomGroupAccessors[0]\n : undefined\n\n // identify groups\n const group =\n aes?.fill || aes?.stroke || aes?.strokeDasharray || aes?.group\n ? defineGroupAccessor(aes)\n : geomGroupAccessor\n\n let hasCategoricalVar = aes.group || geomGroupAccessors.length || false\n const calculatedGroups = group\n ? (Array.from(new Set(data.map(group))) as string[])\n : ['__group']\n\n const thisYAes = aes.y || (geomAesYs.length ? geomAesYs[0] : undefined)\n const resolvedYAes = thisYAes ?? y1Aes ?? y0Aes\n const thisStrokeAes =\n aes.stroke || (geomAesStrokes.length ? geomAesStrokes[0] : undefined)\n const thisFillAes =\n aes.fill || (geomAesFills.length ? geomAesFills[0] : undefined)\n const thisStrokeDasharrayAes =\n aes.strokeDasharray ||\n (geomAesStrokeDasharrays.length ? geomAesStrokeDasharrays[0] : undefined)\n\n /// SCALING ///\n\n let xScale\n const firstX = data.map(aes.x).find((d) => d !== null && d !== undefined)\n if (isDate(firstX)) {\n const domain =\n (xScaleDomain as Date[]) || extent(data, aes.x as (d: unknown) => Date)\n\n const hasDomain =\n typeof domain[0] !== 'undefined' &&\n typeof domain[1] !== 'undefined' &&\n // check for only null Dates\n domain[0].valueOf() !== 0 &&\n domain[1].valueOf() !== 0\n\n xScale = scaleTime()\n .range([margin.left, width - margin.right])\n .domain(hasDomain ? domain : [0, 0])\n } else if (typeof firstX === 'number') {\n const defaultDomain = extent(data, aes.x as (d: unknown) => number)\n\n const domain = (xScaleDomain as number[]) || [\n hasZeroXBaseLine ? 0 : defaultDomain[0],\n defaultDomain[1],\n ]\n\n const hasDomain =\n typeof domain[0] !== 'undefined' && typeof domain[1] !== 'undefined'\n\n const xType: any = xScaleType || scaleLinear\n xScale = xType()\n .range([margin.left, width - margin.right])\n .domain(hasDomain ? domain : [0, 1])\n } else if (!Number.isFinite(firstX) || typeof firstX === 'string') {\n // hasCategoricalVar = true\n // maintain the existing order\n const initialDomain = Array.from(new Set(copiedData.map(aes.x))) as string[]\n const computedDomain = Array.from(new Set(data.map(aes.x))) as string[]\n\n const domain =\n (xScaleDomain as string[]) ||\n computedDomain\n .filter((d) =>\n shouldExcludeMissingXYFromDomains\n ? d !== null && typeof d !== 'undefined'\n : true,\n )\n .sort((a, b) => sortDomain(a, b, initialDomain))\n\n xScale = scaleBand()\n .range([margin.left, width - margin.right])\n .domain(domain)\n }\n if (reverseX) xScale?.domain(xScale.domain().reverse())\n\n let yScale\n\n if (resolvedYAes) {\n const firstY = data\n .map(resolvedYAes)\n .find((d) => d !== null && d !== undefined)\n\n if (isDate(firstY)) {\n const domain =\n (yScaleDomain as Date[]) ||\n extent(data, thisYAes as (d: unknown) => Date)\n\n const hasDomain =\n typeof domain[0] !== 'undefined' && typeof domain[1] !== 'undefined'\n\n yScale = scaleTime()\n .range([height - margin.bottom, margin.top])\n .domain(hasDomain ? domain : [0, 1])\n } else if (typeof firstY === 'number') {\n const defaultDomain = extent(data, thisYAes as (d: unknown) => number)\n\n const domain = yScaleDomain ?? [\n hasZeroYBaseLine ? 0 : defaultDomain[0],\n defaultDomain[1],\n ]\n\n const hasDomain =\n typeof domain[0] !== 'undefined' && typeof domain[1] !== 'undefined'\n\n const yType: any = yScaleType || scaleLinear\n\n yScale = yType()\n .range([height - margin.bottom, margin.top])\n .domain(hasDomain ? domain : [0, 1])\n } else if (!Number.isFinite(firstY) || typeof firstY === 'string') {\n // hasCategoricalVar = true\n // maintain the existing order\n const initialDomain = Array.from(\n new Set(copiedData.map(resolvedYAes)),\n ) as string[]\n const computedDomain = Array.from(\n new Set(data.map(resolvedYAes)),\n ) as string[]\n\n const domain =\n (yScaleDomain as string[]) ||\n computedDomain\n .filter((d) =>\n shouldExcludeMissingXYFromDomains\n ? d !== null && typeof d !== 'undefined'\n : true,\n )\n .sort((a, b) => sortDomain(a, b, initialDomain))\n\n yScale = scaleBand()\n .range([margin.top, height - margin.bottom])\n .domain(domain)\n }\n } else {\n yScale = scaleLinear()\n .range([height - margin.bottom, margin.top])\n .domain([0, 1])\n }\n if (reverseY) yScale?.domain(yScale.domain().reverse())\n\n // fill\n let fillScale\n if (thisFillAes) {\n const firstFill = data\n .map(thisFillAes)\n .find((d) => d !== null && d !== undefined)\n\n const continuousDomain =\n (fillScaleDomain as number[]) ||\n (extent(data, thisFillAes as (d: unknown) => number) as number[])\n\n const continuousInterpolator =\n (fillScaleColors as (t: number) => string) || defaultInterpolator\n\n const categoricalDomain = fillScaleDomain || calculatedGroups\n\n const discreteDomain =\n fillScaleDomain || data.map((d) => (group ? group(d) : '__group'))\n const discreteColors = fillScaleColors || defaultScheme\n const discreteSequential =\n fillScaleColors || createSequentialScheme(continuousInterpolator)\n\n if (fillScaleType) {\n const fillType = fillScaleType as any\n\n if (fillType()?.invertExtent) {\n fillScale = fillType()\n .domain(\n fillType.name === 'quantize' ? continuousDomain : discreteDomain,\n )\n .range(discreteSequential) as VisualEncodingTypes\n } else if (fillType()?.interpolator) {\n fillScale = fillType()\n .domain(continuousDomain)\n .interpolator(continuousInterpolator) as VisualEncodingTypes\n } else {\n hasCategoricalVar = true\n\n fillScale = fillType()\n .domain(categoricalDomain)\n .range(discreteColors) as VisualEncodingTypes\n }\n } else if (!Number.isFinite(firstFill) || typeof firstFill === 'string') {\n hasCategoricalVar = true\n\n fillScale = scaleOrdinal()\n .domain(categoricalDomain)\n .range(discreteColors as string[]) as VisualEncodingTypes\n } else if (isDate(firstFill) || typeof firstFill === 'number') {\n hasCategoricalVar = false\n\n fillScale = scaleSequential()\n .domain(continuousDomain)\n .interpolator(continuousInterpolator) as VisualEncodingTypes\n }\n }\n if (fillScaleReverse && fillScale?.interpolator)\n fillScale?.domain(fillScale.domain().reverse())\n\n // stroke\n let strokeScale\n if (thisStrokeAes) {\n const firstStroke = data\n .map(thisStrokeAes)\n .find((d) => d !== null && d !== undefined)\n\n if (strokeScaleType) {\n let domain\n const strokeType = strokeScaleType as any\n switch (strokeScaleType.name) {\n case 'sequential':\n domain =\n (strokeScaleDomain as number[]) ||\n (extent(data, thisStrokeAes as (d: unknown) => number) as number[])\n\n strokeScale = strokeType()\n .domain(domain)\n .interpolator(\n (strokeScaleColors as (t: number) => string) ||\n defaultInterpolator,\n ) as VisualEncodingTypes\n break\n case 'sequentialLog':\n domain =\n (strokeScaleDomain as number[]) ||\n (extent(data, thisStrokeAes as (d: unknown) => number) as number[])\n\n strokeScale = strokeType()\n .domain(domain)\n .interpolator(\n (strokeScaleColors as (t: number) => string) ||\n defaultInterpolator,\n ) as VisualEncodingTypes\n break\n case 'sequentialSqrt':\n domain =\n (strokeScaleDomain as number[]) ||\n (extent(data, thisStrokeAes as (d: unknown) => number) as number[])\n\n strokeScale = strokeType()\n .domain(domain)\n .interpolator(\n (strokeScaleColors as (t: number) => string) ||\n defaultInterpolator,\n ) as VisualEncodingTypes\n break\n case 'ordinal':\n hasCategoricalVar = true\n domain = (strokeScaleDomain as string[]) || calculatedGroups\n\n strokeScale = strokeType()\n .domain(domain)\n .range(\n (strokeScaleColors as string[]) || defaultScheme,\n ) as VisualEncodingTypes\n break\n default:\n hasCategoricalVar = true\n domain = (strokeScaleDomain as string[]) || calculatedGroups\n\n strokeScale = strokeType()\n .domain(domain)\n .range(\n (strokeScaleColors as string[]) || defaultScheme,\n ) as VisualEncodingTypes\n }\n } else if (\n !Number.isFinite(firstStroke) ||\n typeof firstStroke === 'string'\n ) {\n hasCategoricalVar = true\n const domain = (strokeScaleDomain as string[]) || calculatedGroups\n\n strokeScale = scaleOrdinal()\n .domain(domain)\n .range(\n (strokeScaleColors as string[]) || defaultScheme,\n ) as VisualEncodingTypes\n } else if (isDate(firstStroke) || typeof firstStroke === 'number') {\n const domain =\n (strokeScaleDomain as number[]) ||\n (extent(data, thisStrokeAes as (d: unknown) => number) as number[])\n\n strokeScale = scaleSequential()\n .domain(domain)\n .interpolator(\n (strokeScaleColors as (t: number) => string) || defaultInterpolator,\n ) as VisualEncodingTypes\n }\n }\n if (strokeScaleReverse) strokeScale?.domain(strokeScale.domain().reverse())\n\n // strokeDasharray\n let strokeDasharrayScale\n if (thisStrokeDasharrayAes) {\n hasCategoricalVar = true\n const domain = (strokeDasharrayDomain as string[]) || calculatedGroups\n\n strokeDasharrayScale = scaleOrdinal()\n .domain(domain)\n .range(\n (strokeDasharrays as string[]) || defaultDasharrays,\n ) as VisualEncodingTypes\n }\n\n return {\n xScale,\n yScale,\n fillScale,\n strokeScale,\n strokeDasharrayScale,\n groupAccessor: group,\n groups: hasCategoricalVar\n ? calculatedGroups\n : // ? fillScale?.domain() ?? calculatedGroups\n undefined,\n }\n}\n","import {\n interpolateViridis as defaultInterpolator,\n schemeTableau10,\n} from 'd3-scale-chromatic'\n\nexport const defaultScheme: string[] = [\n schemeTableau10[0],\n schemeTableau10[1],\n schemeTableau10[4],\n schemeTableau10[2],\n schemeTableau10[3],\n ...schemeTableau10.slice(5),\n]\nexport const defaultDasharrays: string[] = [\n '0',\n '2,2',\n '5,4',\n '2,8,2',\n '15,4',\n '8,2,8',\n]\n\nexport { defaultInterpolator }\n\nexport const createSequentialScheme = (\n interpolator = defaultInterpolator,\n n = 5,\n) => {\n const scheme = []\n for (let i = 0; i < n; i += 1) {\n scheme.push(interpolator(i / (n - 1)))\n }\n return scheme\n}\n","import type { Aes } from \"../gg\";\n\nexport const defineGroupAccessor = <Datum>(aes: Aes<Datum>, allowUndefined = false) => {\n if (!aes && allowUndefined) return undefined\n return (\n aes?.fill ||\n aes?.stroke ||\n aes?.strokeDasharray ||\n aes?.group ||\n (allowUndefined ? undefined : () => '__group')\n )\n}","export const debounce = (delay: number, fn: (...args: any[]) => void) => {\n let timerId: ReturnType<typeof setTimeout> | null\n return (...args: any[]) => {\n if (timerId) {\n clearTimeout(timerId)\n }\n timerId = setTimeout(() => {\n fn(...args)\n timerId = null\n }, delay)\n }\n}\n","import { CSSProperties } from 'react'\n\ninterface FocusProps {\n nodes: HTMLCollectionOf<SVGElement>\n focusedIndex: number | number[]\n focusedStyles: CSSProperties\n unfocusedStyles: CSSProperties\n}\n\ninterface UnfocusProps {\n nodes: HTMLCollectionOf<SVGElement>\n baseStyles: CSSProperties\n}\n\nexport const focusNodes = ({\n nodes,\n focusedIndex,\n focusedStyles,\n unfocusedStyles,\n}: FocusProps) => {\n const styleNodes = nodes\n\n const focusedIndices =\n typeof focusedIndex !== 'undefined' ? [focusedIndex].flat() : undefined\n\n const toUnfocus = Array.from(nodes)?.filter(\n (_, ind) => focusedIndices && !focusedIndices.includes(ind),\n )\n\n toUnfocus?.forEach((node) => {\n const styleNode = node\n Object.entries(unfocusedStyles).forEach(([key, val]) => {\n styleNode.style[key as any] = val as string\n })\n })\n\n Object.entries(focusedStyles).forEach(([key, val]) => {\n focusedIndices?.forEach((ind) => {\n styleNodes[ind].style[key as any] = val as string\n })\n })\n}\n\nexport const unfocusNodes = ({ nodes, baseStyles }: UnfocusProps) => {\n Array.from(nodes).forEach((node) => {\n const styleNode = node\n Object.entries(baseStyles).forEach(([key, val]) => {\n styleNode.style[key as any] = val as string\n })\n })\n}\n","export const getYAlongPath = (\n x: number,\n path: SVGPathElement,\n error = 0.01\n) => {\n let lengthEnd = path.getTotalLength()\n let lengthStart = 0\n let point = path.getPointAtLength((lengthEnd + lengthStart) / 2) // get the middle point\n const bisectionIterationsMax = 50\n let bisectionIterations = 0\n\n while (x < point.x - error || x > point.x + error) {\n // get the middle point\n point = path.getPointAtLength((lengthEnd + lengthStart) / 2)\n\n if (x < point.x) {\n lengthEnd = (lengthStart + lengthEnd) / 2\n } else {\n lengthStart = (lengthStart + lengthEnd) / 2\n }\n\n bisectionIterations += 1\n\n // Increase iteration\n if (bisectionIterationsMax < bisectionIterations) break\n }\n return point.y\n}\n","export const elongate = (\n data: { [key: string]: any }[],\n keyName: string,\n valName: string,\n exclude: string[]\n) => {\n const longer: any[] = []\n data.forEach((d) => {\n const keys = Object.keys(d).filter((k) => !exclude.includes(k))\n const keepAsIs = Object.keys(d).reduce((object: any, key) => {\n const newObj = object\n if (exclude.includes(key)) {\n newObj[key] = d[key]\n }\n return newObj\n }, {})\n keys.forEach((k) => {\n const out: { [key: string]: any } = {}\n out[keyName] = k\n out[valName] = d[k]\n longer.push({ ...out, ...keepAsIs })\n })\n })\n return longer\n}\n","import { isDate } from \"./dates\"\n\nexport const widen = <Datum>(\n data: Datum[],\n pivot: (d: Datum) => any,\n getGroup: (d: Datum) => any,\n count: (d: Datum) => any\n) => {\n const pivots = Array.from(new Set(data.map(d => isDate(pivot(d)) ? pivot(d).valueOf() : pivot(d))))\n const groups = Array.from(new Set(data.map(getGroup)))\n return pivots.map((p, i) => {\n const out: any = { key: isDate(p) ? p.valueOf() : p, i }\n groups.forEach((g) => {\n const pivotGroup = data.find(\n d =>\n (isDate(pivot(d))\n ? pivot(d).valueOf() === p.valueOf() && getGroup(d) === g\n : pivot(d) === p && getGroup(d) === g)\n )\n if (pivotGroup) {\n out[g] = count(pivotGroup) ?? undefined\n }\n })\n return out\n })\n}\n","import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react'\nimport { ScaleBand } from 'd3-scale'\nimport { Delaunay } from 'd3-delaunay'\nimport { SetStateAction, useAtom } from 'jotai'\nimport { pointer } from 'd3-selection'\nimport { extent, max, min } from 'd3-array'\nimport {\n tooltipState,\n themeState,\n xScaleState,\n yScaleState,\n zoomState,\n TooltipProps,\n} from '../atoms'\nimport { Aes, DataValue, BrushAction } from '../gg/types'\nimport { useGG } from '../gg/GGBase'\nimport { ZoomOutButton, useUnZoom } from '../gg/zoom'\nimport {\n BrushCoords,\n isBetween,\n ExclusionArea,\n BrushExclusion,\n} from './brushing'\n\ninterface StackMidpoint<X, Y> {\n groupVal: string\n xVal: X\n yVal: Y\n}\n\ninterface EventAreaProps<Datum> {\n x: (d: Datum) => number | undefined\n y: (d: Datum) => number | undefined\n group?: 'x' | 'y'\n xAdj?: number\n yAdj?: number\n onMouseOver?: ({ d, i }: { d: Datum[]; i: number[] }) => void\n onClick?: ({ d, i }: { d: Datum[]; i: number[] }) => void\n onMouseLeave: () => void\n onDatumFocus?: (data: Datum[], index: number[]) => void\n data?: Datum[]\n stackXMidpoints?: StackMidpoint<string | number, string | number>[]\n stackYMidpoints?: StackMidpoint<string | number, string | number>[]\n xBandScale?: ScaleBand<string>\n yBandScale?: ScaleBand<string>\n aes?: Omit<Aes<Datum>, 'x'> & {\n x?: DataValue<Datum>\n y0?: DataValue<Datum>\n y1?: DataValue<Datum>\n }\n customXExtent?: (number | undefined)[]\n customYExtent?: (number | undefined)[]\n getYValExtent?: (data: Datum[]) => (number | undefined)[]\n positionKeys?: string\n disabled?: boolean\n fill?: 'x' | 'y'\n showTooltip?: boolean\n brushAction?: BrushAction\n isZoomedOut?: boolean\n}\n\nconst BUFFER = 2\n\nexport const EventArea = <Datum,>({\n x,\n y,\n group,\n xAdj = 0,\n yAdj = 0,\n onMouseOver,\n onClick,\n onMouseLeave,\n onDatumFocus,\n data,\n aes,\n customXExtent,\n customYExtent,\n getYValExtent,\n positionKeys,\n disabled,\n showTooltip = true,\n brushAction,\n isZoomedOut,\n stackXMidpoints,\n stackYMidpoints,\n xBandScale,\n yBandScale,\n fill,\n}: EventAreaProps<Datum>) => {\n const { ggState } = useGG() || {}\n const {\n width,\n height,\n margin,\n data: ggData,\n scales,\n id,\n } = ggState || {\n width: 0,\n height: 0,\n margin: {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n },\n }\n\n const [{ datum: ttDatum }, setTooltip] = useAtom<\n TooltipProps<Datum>,\n SetStateAction<TooltipProps<Datum>>,\n void\n >(tooltipState)\n const [{ animationDuration, geoms }] = useAtom(themeState)\n const [{ domain: givenYDomain, reverse: reverseY }, setYScale] =\n useAtom(yScaleState)\n const [{ reverse: reverseX }, setXScale] = useAtom(xScaleState)\n const [\n { xDomain: xZoomDomain, yDomain: yZoomDomain, onZoom, onUnzoom },\n setZoom,\n ] = useAtom(zoomState)\n\n const unZoom = useUnZoom()\n\n const rectRef = useRef<SVGRectElement>(null)\n const readyToFocusRef = useRef(false)\n const isHeldDownRef = useRef(false)\n const heldDownTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)\n const brushCoords = useRef<BrushCoords>()\n\n const exclusionTopRef = useRef<SVGRectElement | null>(null)\n const exclusionRightRef = useRef<SVGRectElement | null>(null)\n const exclusionBottomRef = useRef<SVGRectElement | null>(null)\n const exclusionLeftRef = useRef<SVGRectElement | null>(null)\n\n const xGrouped = useMemo(() => group === 'x', [group])\n const yGrouped = useMemo(() => group === 'y', [group])\n\n const isVoronoi = useMemo(() => !!onDatumFocus, [onDatumFocus])\n\n const [isBrushing, setIsBrushing] = useState(false)\n\n const xVals = data?.map(x)\n const yVals = data?.map(y)\n\n useEffect(() => {\n readyToFocusRef.current = false\n const duration = animationDuration ?? 1000\n const timeout = setTimeout(() => {\n readyToFocusRef.current = true\n }, duration + 50)\n\n return () => clearTimeout(timeout)\n }, [\n // disable focusing in event area when data is changing\n JSON.stringify(ggData),\n JSON.stringify(data),\n // disable focusing when coordinate mapping is changing\n JSON.stringify(xVals),\n JSON.stringify(yVals),\n width,\n animationDuration,\n xZoomDomain,\n yZoomDomain,\n positionKeys,\n ])\n\n const hasCategoricalAxis = useMemo(\n () =>\n typeof scales?.xScale.domain()[0] === 'string' ||\n typeof scales?.yScale.domain()[0] === 'string',\n [scales?.xScale, scales?.yScale],\n )\n\n const delaunayData = useMemo(() => data ?? [], [data])\n const delaunayX = useCallback((v: any) => (x(v) ?? 0) + xAdj, [x, xAdj])\n const delaunayY = useCallback((v: any) => (y(v) ?? 0) + yAdj, [y, yAdj])\n\n const delaunay = useMemo(\n () => Delaunay.from(delaunayData, delaunayX, delaunayY),\n [data, delaunayX, delaunayY],\n )\n\n const xDelaunays = useMemo(() => {\n if (!stackYMidpoints) return undefined\n\n const delaunays = xBandScale?.domain().map((xVal) => {\n const thisX = scales?.xScale(xVal)\n\n const xGroupData = stackYMidpoints.filter(\n (s) => s.xVal === xVal.valueOf(),\n )\n\n return {\n delaunay: Delaunay.from(\n xGroupData,\n (v) => scales?.xScale(v.xVal) ?? 0,\n (v) => scales?.yScale(v.yVal) as number,\n ),\n xVal: thisX,\n data: xGroupData,\n }\n })\n if (!hasCategoricalAxis) {\n return delaunays?.sort((a, b) => (a.xVal ?? 0) - (b.xVal ?? 0))\n }\n return delaunays\n }, [\n stackYMidpoints,\n scales?.yScale,\n xBandScale,\n hasCategoricalAxis,\n scales?.xScale,\n xAdj,\n ])\n\n const xVoronois = useMemo(() => {\n if (!xDelaunays || !isVoronoi) return undefined\n\n const dx = (xBandScale?.step?.() ?? 0) / 2\n\n return xDelaunays.map((xd) => ({\n voronoi: xd.delaunay.voronoi([\n (xd?.xVal ?? 0) + xAdj - dx,\n margin.top,\n (xd?.xVal ?? 0) + dx + xAdj,\n height - margin.bottom,\n ]),\n data: xd.data as Datum[],\n }))\n }, [xDelaunays, scales?.xScale, xBandScale, xAdj, width, margin])\n\n const yDelaunays = useMemo(() => {\n if (!stackXMidpoints) return undefined\n\n const delaunays = yBandScale?.domain().map((yVal) => {\n const thisY = scales?.yScale(yVal)\n const yGroupData = stackXMidpoints.filter((s) => s.yVal === yVal)\n\n return {\n delaunay: Delaunay.from(\n [...yGroupData],\n (v) => scales?.xScale(v.xVal) as number,\n (v) => scales?.yScale(v.yVal) as number,\n ),\n yVal: thisY,\n data: yGroupData,\n }\n })\n if (!hasCategoricalAxis) {\n return delaunays?.sort((a, b) => (a.yVal ?? 0) - (b.yVal ?? 0))\n }\n return delaunays\n }, [\n stackXMidpoints,\n scales?.yScale,\n scales?.xScale,\n yBandScale,\n hasCategoricalAxis,\n ])\n\n const yVoronois = useMemo(() => {\n const isValid = width - (margin.left + margin.right) > 0\n if (!yDelaunays || !isVoronoi || !isValid) return undefined\n\n const dy = (yBandScale?.step?.() ?? 0) / 2\n\n return yDelaunays.map((yd) => ({\n voronoi: yd.delaunay.voronoi([\n margin.left,\n (yd.yVal ?? 0) - dy + yAdj,\n width - margin.right,\n (yd.yVal ?? 0) + dy + yAdj,\n ]),\n data: yd.data as Datum[],\n }))\n }, [yDelaunays, scales?.yScale, yAdj, width, margin])\n\n const voronoi = useMemo(() => {\n const isValid =\n width - (margin.left + margin.right) > 0 &&\n height - (margin.bottom + margin.top) > 0\n if (!isVoronoi || !isValid) return undefined\n\n return delaunay.voronoi([\n margin.left,\n margin.top,\n width - margin.right,\n height - margin.bottom,\n ])\n }, [delaunay, isVoronoi])\n\n const resetTooltip = useCallback(() => {\n setTooltip((prev) => ({\n ...prev,\n datum: undefined,\n }))\n }, [setTooltip])\n\n const resetBrush = useCallback(() => {\n if (exclusionLeftRef.current) {\n exclusionLeftRef.current.setAttribute('width', '0px')\n }\n setIsBrushing(false)\n }, [setIsBrushing])\n\n const handleBrush = useCallback(\n (posX: number, posY: number) => {\n if (isHeldDownRef.current && brushCoords.current) {\n brushCoords.current = {\n ...brushCoords.current,\n x1: posX,\n y1: posY,\n }\n\n const { x0, x1, y0, y1 } = brushCoords.current\n\n const xRange = scales?.xScale.range()\n const yRange = scales?.yScale.range()\n\n const xStart =\n (yDelaunays || yGrouped) && xRange ? xRange[0] : Math.min(x0, x1)\n const xEnd =\n (yDelaunays || yGrouped) && xRange ? xRange[1] : Math.max(x0, x1)\n const yStart =\n (xDelaunays || xGrouped) && yRange\n ? yRange[1] - BUFFER\n : Math.min(y0, y1)\n const yEnd =\n (xDelaunays || xGrouped) && yRange\n ? yRange[0] + BUFFER\n : Math.max(y0, y1)\n\n if (exclusionLeftRef.current) {\n exclusionLeftRef.current.setAttribute(\n 'x',\n `${margin.left - BUFFER}px`,\n )\n exclusionLeftRef.current.setAttribute('y', `${yStart}px`)\n exclusionLeftRef.current.setAttribute(\n 'width',\n `${Math.max(xStart - margin.left + BUFFER, 0)}px`,\n )\n exclusionLeftRef.current.setAttribute('height', `${yEnd - yStart}px`)\n }\n if (exclusionRightRef.current) {\n exclusionRightRef.current.setAttribute('x', `${xEnd}px`)\n exclusionRightRef.current.setAttribute('y', `${yStart}px`)\n exclusionRightRef.current.setAttribute(\n 'width',\n `${Math.max(width - margin.right - xEnd + BUFFER, 0)}px`,\n )\n exclusionRightRef.current.setAttribute('height', `${yEnd - yStart}px`)\n }\n if (exclusionTopRef.current) {\n exclusionTopRef.current.setAttribute('x', `${margin.left - BUFFER}px`)\n exclusionTopRef.current.setAttribute('y', `${margin.top - BUFFER}px`)\n exclusionTopRef.current.setAttribute(\n 'width',\n `${width - margin.right - margin.left + BUFFER * 2}px`,\n )\n exclusionTopRef.current.setAttribute(\n 'height',\n `${Math.max(yStart - margin.top + BUFFER, 0)}px`,\n )\n }\n if (exclusionBottomRef.current) {\n exclusionBottomRef.current.setAttribute(\n 'x',\n `${margin.left - BUFFER}px`,\n )\n exclusionBottomRef.current.setAttribute('y', `${yEnd}px`)\n exclusionBottomRef.current.setAttribute(\n 'width',\n `${width - margin.right - margin.left + BUFFER * 2}px`,\n )\n exclusionBottomRef.current.setAttribute(\n 'height',\n `${Math.max(height - yEnd - margin.bottom + BUFFER, 0)}px`,\n )\n }\n }\n },\n [xGrouped, yGrouped, margin, scales, xDelaunays, yDelaunays],\n )\n\n const handleBrushStop = useCallback(\n (\n event:\n | React.MouseEvent<SVGPathElement>\n | React.MouseEvent<SVGGElement>\n | React.PointerEvent<SVGRectElement>\n | React.MouseEvent<HTMLButtonElement>,\n ) => {\n event.preventDefault()\n if (isHeldDownRef.current && brushCoords.current) {\n const { x0, x1, y0, y1 } = brushCoords.current\n\n resetTooltip()\n resetBrush()\n\n const brushedData = data?.filter((d) => {\n const xVal = x(d)\n const yVal = y(d)\n\n if (xGrouped || xDelaunays) return isBetween(xVal, x0, x1)\n if (yGrouped || yDelaunays) return isBetween(yVal, y0, y1)\n return isBetween(xVal, x0, x1) && isBetween(yVal, y0, y1)\n })\n\n const hasXVals = brushedData?.some((v) => aes?.x?.(v))\n const hasYVals = brushedData?.some((v) => aes?.y?.(v) ?? aes?.y0?.(v))\n\n if (brushedData && brushedData.length && hasXVals && hasYVals) {\n let newXDomain = [\n scales?.xScale.invert(Math.min(x0, x1)),\n scales?.xScale.invert(Math.max(x0, x1)),\n ]\n\n newXDomain = reverseX ? newXDomain.reverse() : newXDomain\n\n const brushedYExtent = getYValExtent\n ? getYValExtent(brushedData)\n : extent(\n brushedData\n .map((d) => {\n const yVal = (aes?.y && aes.y(d)) as number\n const y0Val = (aes?.y0 && aes.y0(d)) as number\n const y1Val = (aes?.y1 && aes.y1(d)) as number\n\n return extent([yVal, y0Val, y1Val])\n })\n .flat() as number[],\n )\n\n let reconciledYExtent = givenYDomain\n ? [\n max([brushedYExtent[0], givenYDomain[0]] as [number, number]),\n min([brushedYExtent[1], givenYDomain[1]] as [number, number]),\n ]\n : brushedYExtent\n\n reconciledYExtent = reverseY\n ? reconciledYExtent\n : reconciledYExtent.reverse()\n\n let newYDomain = xGrouped\n ? reconciledYExtent\n : [\n scales?.yScale.invert(Math.min(y0, y1)),\n scales?.yScale.invert(Math.max(y0, y1)),\n ]\n\n newYDomain = reverseY ? newYDomain : newYDomain.reverse()\n\n // TODO: do nothing if sufficiently zoomed in already\n // e.g. 50-100X in either x/y directions\n\n setXScale((prev) => ({\n ...prev,\n domain: newXDomain,\n }))\n setYScale((prev) => ({\n ...prev,\n domain: newYDomain,\n }))\n setZoom((prev) => ({\n ...prev,\n xDomain: {\n ...prev.xDomain,\n current: newXDomain,\n },\n yDomain: {\n ...prev.yDomain,\n current: newYDomain,\n },\n }))\n\n if (onZoom) onZoom({ x: newXDomain, y: newYDomain })\n }\n }\n isHeldDownRef.current = false\n if (heldDownTimeout.current) clearTimeout(heldDownTimeout.current)\n },\n [\n resetTooltip,\n resetBrush,\n ggData,\n xGrouped,\n yGrouped,\n xDelaunays,\n yDelaunays,\n reverseX,\n reverseY,\n aes,\n scales,\n y,\n xZoomDomain,\n yZoomDomain,\n onZoom,\n geoms,\n getYValExtent,\n ],\n )\n\n const handleMouseOver = useCallback(\n (\n event:\n | React.MouseEvent<SVGRectElement>\n | React.PointerEvent<SVGRectElement>,\n ) => {\n if (readyToFocusRef.current && data && data.length) {\n const [pointerX, pointerY] = pointer(event, rectRef.current)\n const [posX, posY] = [Math.floor(pointerX), Math.floor(pointerY)]\n\n if (isHeldDownRef.current && brushAction && !hasCategoricalAxis) {\n handleBrush(posX, posY)\n } else if (showTooltip) {\n let ind = delaunay.find(posX, posY)\n\n if (xDelaunays) {\n const xGroupWidth = xBandScale?.step?.() ?? 1\n const adjPosX =\n (posX -\n margin.left +\n ((xBandScale?.padding?.() ?? 0) * xGroupWidth) / 2) /\n xGroupWidth\n const xGroupIndex = Math.min(\n Math.floor(Math.max(0, adjPosX)),\n xDelaunays.length - 1,\n )\n const xStackIndex = xDelaunays[xGroupIndex].delaunay.find(\n posX,\n posY,\n )\n const xStackDatum = xDelaunays[xGroupIndex].data[xStackIndex]\n ind = data.findIndex(\n (d) =>\n aes?.x?.(d)?.valueOf() === xStackDatum.xVal &&\n scales?.groupAccessor?.(d) === xStackDatum.groupVal,\n )\n }\n\n if (yDelaunays) {\n const yGroupHeight = yBandScale?.step?.() ?? 1\n const adjPosY =\n posY +\n margin.top -\n yAdj +\n ((yBandScale?.padding?.() ?? 0) * yGroupHeight) / 2\n const yGroupIndex = Math.min(\n Math.floor(Math.max(0, adjPosY) / yGroupHeight),\n yDelaunays.length - 1,\n )\n const yStackIndex = yDelaunays[yGroupIndex].delaunay.find(\n posX,\n posY,\n )\n const yStackDatum = yDelaunays[yGroupIndex].data[yStackIndex]\n ind = data.findIndex(\n (d) =>\n aes?.y?.(d) === yStackDatum.yVal &&\n scales?.groupAccessor?.(d) === yStackDatum.groupVal,\n )\n }\n\n const datum = data[ind]\n\n const xDomain = scales?.xScale.domain() as any[]\n\n const yDomain = scales?.yScale.domain() as any[]\n const datumInXRange =\n ['x', 'y'].includes(fill ?? '') ||\n (aes?.x &&\n xDomain &&\n (xDomain.includes(aes?.x(datum)?.valueOf()) ||\n isBetween(\n aes?.x(datum)?.valueOf() as number,\n xDomain[0],\n xDomain[1],\n )))\n\n const datumInYRange =\n ['x', 'y'].includes(fill ?? '') ||\n (aes?.y &&\n yDomain &&\n (yDomain.includes(aes?.y(datum)) ||\n isBetween(aes?.y(datum) as number, yDomain[0], yDomain[1])))\n\n if (xGrouped && aes?.x && datumInXRange) {\n const left = x(datum)\n\n // skip if the data hasn't changed\n if (ttDatum && x(ttDatum[0]) === left) return\n\n const groupDatum: Datum[] = []\n const groupDatumInd: number[] = []\n\n data.forEach((d, i) => {\n if (aes.x && aes.x(d)?.toString() === aes.x(datum)?.toString()) {\n groupDatum.push(d)\n groupDatumInd.push(i)\n }\n })\n\n const tooltips = document.getElementsByClassName(\n `__gg-tooltip-${id}`,\n ) as HTMLCollectionOf<SVGGElement>\n Array.from(tooltips).forEach((m) => {\n const thisTooltip = m\n thisTooltip.style.transform = `translate(${left}px, 0)`\n })\n\n if (onMouseOver) onMouseOver({ d: groupDatum, i: groupDatumInd })\n setTooltip((prev) => ({\n ...prev,\n datum: groupDatum,\n }))\n } else if (yGrouped && aes?.y && datumInYRange) {\n // skip if the data hasn't changed\n if (ttDatum && y(ttDatum[0]) === y(datum)) return\n\n const groupDatum: Datum[] = []\n const groupDatumInd: number[] = []\n\n data.forEach((d, i) => {\n if (aes?.y && aes.y(d)?.toString() === aes.y(datum)?.toString()) {\n groupDatum.push(d)\n groupDatumInd.push(i)\n }\n })\n\n if (onMouseOver) onMouseOver({ d: groupDatum, i: groupDatumInd })\n setTooltip((prev) => ({\n ...prev,\n datum: groupDatum,\n }))\n } else if (datumInXRange && datumInYRange) {\n if (onMouseOver) onMouseOver({ d: [datum], i: [ind] })\n\n setTooltip((prev) => ({\n ...prev,\n datum: [datum],\n }))\n }\n }\n }\n },\n [\n data,\n aes,\n setTooltip,\n width,\n delaunay,\n yDelaunays,\n xDelaunays,\n onMouseOver,\n xGrouped,\n yGrouped,\n ttDatum,\n scales,\n xBandScale,\n yBandScale,\n handleBrush,\n brushAction,\n hasCategoricalAxis,\n fill,\n margin.top,\n margin.left,\n ],\n )\n\n const handleMouseOut = useCallback(\n (\n event:\n | React.MouseEvent<SVGRectElement>\n | React.MouseEvent<SVGGElement>\n | React.PointerEvent<SVGRectElement>\n | React.MouseEvent<HTMLButtonElement>,\n ) => {\n if (readyToFocusRef.current) {\n if (onMouseLeave) onMouseLeave()\n if (showTooltip) resetTooltip()\n if (isBrushing) handleBrushStop(event)\n }\n document.onselectstart = () => true\n },\n [showTooltip, resetTooltip, onMouseLeave, isBrushing],\n )\n\n const handleUnbrush = useCallback(\n (\n event:\n | React.MouseEvent<SVGRectElement>\n | React.MouseEvent<SVGPathElement>\n | React.MouseEvent<HTMLButtonElement>,\n ) => {\n handleMouseOut(event)\n\n if (brushAction === BrushAction.ZOOM) {\n unZoom({ customXExtent, customYExtent })\n }\n\n if (showTooltip) resetTooltip()\n if (brushAction) resetBrush()\n if (onUnzoom) onUnzoom()\n },\n [\n handleMouseOut,\n resetTooltip,\n resetBrush,\n setYScale,\n setXScale,\n setZoom,\n customYExtent,\n yZoomDomain?.original,\n xZoomDomain?.original,\n brushAction,\n showTooltip,\n onUnzoom,\n unZoom,\n geoms,\n ],\n )\n\n const handleClick = useCallback(\n (event: React.MouseEvent<SVGRectElement>) => {\n const [posX, posY] = pointer(event, rectRef.current)\n\n document.onselectstart = () => false\n\n if (event.detail > 1) event.preventDefault()\n\n if (data && data.length && brushAction && !hasCategoricalAxis) {\n heldDownTimeout.current = setTimeout(() => {\n onMouseLeave()\n resetTooltip()\n setIsBrushing(true)\n isHeldDownRef.current = true\n brushCoords.current = {\n x0: posX,\n x1: posX,\n y0: posY,\n y1: posY,\n }\n }, 180)\n }\n\n if (onClick && data && data.length) {\n const ind = delaunay.find(posX, posY)\n const datum = data[ind]\n\n if (xGrouped && aes?.x) {\n const groupDatum: Datum[] = []\n const groupDatumInd: number[] = []\n\n data.forEach((d, i) => {\n if (aes.x && aes.x(d)?.valueOf() === aes.x(datum)?.valueOf()) {\n groupDatum.push(d)\n groupDatumInd.push(i)\n }\n })\n onClick({ d: groupDatum, i: groupDatumInd })\n } else if (yGrouped && aes?.y) {\n const groupDatum: Datum[] = []\n const groupDatumInd: number[] = []\n\n data.forEach((d, i) => {\n if (aes?.y && aes.y(d)?.toString() === aes.y(datum)?.toString()) {\n groupDatum.push(d)\n groupDatumInd.push(i)\n }\n })\n\n onClick({ d: groupDatum, i: groupDatumInd })\n } else {\n onClick({ d: [datum], i: [ind] })\n }\n }\n return width\n },\n [\n data,\n width,\n onClick,\n delaunay,\n aes,\n group,\n onMouseLeave,\n resetTooltip,\n brushAction,\n hasCategoricalAxis,\n ],\n )\n\n const handleVoronoiMouseOver = useCallback(\n (voronoiData: Datum[], i: number) => {\n if (\n readyToFocusRef.current &&\n voronoiData &&\n voronoiData.length &&\n !isBrushing\n ) {\n const datum = voronoiData[i]\n const focusedData: Datum[] = []\n const focusedIndexes: number[] = []\n\n if (xGrouped && aes?.x) {\n voronoiData.forEach((vd, ind) => {\n if (aes?.x && aes.x(vd)?.toString() === aes.x(datum)?.toString()) {\n focusedData.push(vd)\n focusedIndexes.push(ind)\n }\n })\n } else if (yGrouped && aes?.y) {\n voronoiData.forEach((vd, ind) => {\n if (aes?.y && aes.y(vd)?.toString() === aes.y(datum)?.toString()) {\n focusedData.push(vd)\n focusedIndexes.push(ind)\n }\n })\n } else if (data && yDelaunays) {\n const vd = datum as StackMidpoint<string | number, string | number>\n\n data.forEach((d, ind) => {\n if (\n aes?.y?.(d) === vd.yVal &&\n scales?.groupAccessor?.(d) === vd.groupVal\n ) {\n focusedData.push(d)\n focusedIndexes.push(ind)\n }\n })\n } else if (data && xDelaunays) {\n const vd = datum as StackMidpoint<string | number, string | number>\n\n data.forEach((d, ind) => {\n if (\n aes?.x?.(d)?.valueOf() === vd.xVal &&\n scales?.groupAccessor?.(d) === vd.groupVal\n ) {\n focusedData.push(d)\n focusedIndexes.push(ind)\n }\n })\n } else {\n focusedData.push(datum)\n focusedIndexes.push(i)\n }\n\n setTooltip((prev) => ({\n ...prev,\n datum: focusedData,\n }))\n\n if (onMouseOver) onMouseOver({ d: focusedData, i: focusedIndexes })\n if (onDatumFocus) onDatumFocus(focusedData, focusedIndexes)\n }\n },\n [\n isBrushing,\n onMouseOver,\n onDatumFocus,\n yGrouped,\n yDelaunays,\n aes?.y,\n scales?.groupAccessor,\n setTooltip,\n ],\n )\n\n return (\n <>\n <g>\n {!disabled && (\n <>\n <clipPath id={`__gg_canvas_${id}`}>\n <rect\n width={width - margin.right - margin.left + BUFFER * 2}\n height={height - margin.bottom - margin.top + BUFFER * 2}\n x={margin.left - BUFFER}\n y={margin.top - BUFFER}\n fill=\"transparent\"\n />\n </clipPath>\n <rect\n ref={rectRef}\n width={width - margin.right - margin.left + BUFFER * 2}\n height={height - margin.bottom - margin.top + BUFFER}\n x={margin.left - BUFFER}\n y={margin.top - BUFFER}\n // stroke=\"tomato\"\n fill=\"transparent\"\n onMouseMove={handleMouseOver}\n onMouseLeave={handleMouseOut}\n onPointe