@graphique/geom-point
Version:
For points, scatterplots, and bubbles
1 lines • 71.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/geomPoint.tsx","../src/types/index.ts","../src/tooltip/Tooltip.tsx","../src/tooltip/DefaultTooltip.tsx","../src/legend/AppearanceLegend.tsx","../src/legend/CategoricalLegend.tsx","../src/legend/ColorBandLegend.tsx","../src/legend/SizeLegend.tsx"],"sourcesContent":["export { GeomPoint, type GeomProps } from './geomPoint'\nexport {\n Legend,\n SizeLegend,\n type LegendProps,\n type SizeLegendProps,\n} from './legend'\nexport { type GeomAes, Entrance, SizeLabelDirection } from './types'\n","import React, {\n useState,\n useEffect,\n useMemo,\n useCallback,\n CSSProperties,\n SVGAttributes,\n useRef,\n} from 'react'\nimport { NodeGroup } from 'react-move'\nimport { easeCubic } from 'd3-ease'\nimport { scaleSqrt } from 'd3-scale'\nimport { extent } from 'd3-array'\nimport { interpolate } from 'd3-interpolate'\nimport { useAtom } from 'jotai'\nimport {\n useGG,\n focusNodes,\n unfocusNodes,\n EventArea,\n themeState,\n radiusScaleState,\n zoomState,\n xScaleState,\n yScaleState,\n isDate,\n defineGroupAccessor,\n Aes,\n BrushAction,\n usePageVisibility,\n} from '@graphique/graphique'\nimport { Entrance, type GeomAes } from './types'\nimport { Tooltip } from './tooltip'\n\nexport interface GeomProps<Datum> {\n /**\n * **data used by this Geom**\n *\n * This will overwrite top-level `data` passed to `GG` as it relates to mappings defined in `aes`.\n */\n data?: Datum[]\n /**\n * **functional mapping applied to `data` for this Geom**\n *\n * This extends the top-level `aes` passed to `GG`. Any repeated mappings defined here will take precedence within the Geom.\n */\n aes?: GeomAes<Datum>\n /** attributes passed to the underlying SVG elements */\n attr?: SVGAttributes<SVGCircleElement>\n /** should this Geom have a tooltip associated with it (_default_: `true`) */\n showTooltip?: boolean\n /** determines what happens when brushing (clicking and dragging) over the drawing area */\n brushAction?: BrushAction\n /** where elements should start as they enter the drawing area (_default_: `Entrance.BOTTOM`) */\n entrance?: Entrance\n /** should elements be strictly clipped at the bounds of the drawing area (_default_: `false`) */\n isClipped?: boolean\n /** array of keys (of the kind that are generated by `aes.key`) used to programmatically focus associated points */\n focusedKeys?: string[]\n /** styles applied to focused elements */\n focusedStyle?: CSSProperties\n /** styles applied to unfocused elements */\n unfocusedStyle?: CSSProperties\n /** callback called for mousemove events on the drawing area when focusing data */\n onDatumFocus?: (data: Datum[], index: number[]) => void\n /** callback called for click events on the drawing area when selecting focused data */\n onDatumSelection?: (data: Datum[], index: number[]) => void\n /** callback called for mouseleave events on the drawing area */\n onExit?: () => void\n /** should elements enter/update/exit with animated transitions (_default_: `true`) */\n isAnimated?: boolean\n}\n\nconst GeomPoint = <Datum,>({\n data: localData,\n aes: localAes,\n focusedStyle,\n unfocusedStyle,\n attr,\n focusedKeys = [],\n onDatumFocus,\n onDatumSelection,\n entrance = Entrance.BOTTOM,\n onExit,\n showTooltip = true,\n brushAction,\n isClipped = false,\n isAnimated = true,\n}: GeomProps<Datum>) => {\n const { ggState } = useGG<Datum>() || {}\n const { id, data, aes, scales, copiedScales, height, margin } = ggState || {}\n\n const [theme, setTheme] = useAtom(themeState)\n const [radiusScale] = useAtom(radiusScaleState)\n const [{ xDomain: xZoomDomain, yDomain: yZoomDomain }] = useAtom(zoomState)\n const [{ isFixed: isFixedX }] = useAtom(xScaleState)\n const [{ isFixed: isFixedY }] = useAtom(yScaleState)\n\n const isVisible = usePageVisibility()\n\n const baseAttr: SVGAttributes<SVGCircleElement> = {\n r: 3.5,\n fillOpacity: 1,\n strokeOpacity: 1,\n }\n\n const geomAttr: SVGAttributes<SVGCircleElement> = {\n ...baseAttr,\n ...attr,\n }\n\n const { domain: sizeDomain, range: sizeRange } = radiusScale || {}\n const { defaultFill, animationDuration: duration } = theme\n\n const initialGeomData = useMemo(() => localData || data, [data, localData])\n\n const geomAes = useMemo(() => {\n if (localAes) {\n return {\n ...aes,\n ...localAes,\n }\n }\n return aes\n }, [aes, localAes])\n\n const group = useMemo(\n () => geomAes && defineGroupAccessor(geomAes as Aes<Datum>),\n [geomAes, defineGroupAccessor],\n )\n\n const positionKeyAccessor = useCallback(\n (d: Datum) =>\n `${geomAes?.x && geomAes.x(d)}-${geomAes?.y && geomAes.y(d)}-${\n group && group(d)\n }` as string,\n [geomAes, group],\n )\n\n const keyAccessor = useCallback(\n (d: Datum) => (geomAes?.key ? geomAes.key(d) : positionKeyAccessor(d)),\n [geomAes, group, positionKeyAccessor],\n )\n\n const undefinedX = useMemo(\n () =>\n initialGeomData\n ? initialGeomData.filter(\n (d) =>\n geomAes?.x &&\n (geomAes.x(d) === null ||\n typeof geomAes.x(d) === 'undefined' ||\n (isDate(geomAes.x(d)) &&\n Number.isNaN(geomAes.x(d)?.valueOf()))),\n )\n : [],\n [initialGeomData, geomAes],\n )\n const undefinedY = useMemo(\n () =>\n initialGeomData\n ? initialGeomData.filter(\n (d) =>\n geomAes?.y &&\n (geomAes.y(d) === null || typeof geomAes.y(d) === 'undefined'),\n )\n : [],\n [initialGeomData],\n )\n\n const validFocusedKeys = useMemo(\n () => focusedKeys.filter((v) => v),\n [focusedKeys],\n )\n\n const geomData = useMemo(() => {\n const presentData = initialGeomData?.filter(\n (d) =>\n geomAes?.x &&\n geomAes?.x(d) !== null &&\n !(typeof geomAes?.x(d) === 'undefined') &&\n (isDate(geomAes?.x(d))\n ? !Number.isNaN(geomAes?.x(d)?.valueOf())\n : true) &&\n geomAes.y &&\n geomAes.y(d) !== null &&\n !(typeof geomAes.y(d) === 'undefined'),\n )\n\n const uniqueKeyVals = Array.from(\n new Set(presentData?.map((d) => keyAccessor(d))),\n )\n\n return uniqueKeyVals.flatMap((k) => {\n const dataWithKey = presentData?.filter((d) => keyAccessor(d) === k)\n if (dataWithKey && dataWithKey.length > 1) {\n return dataWithKey.map((dk: any, i) => ({\n ...dk,\n gg_gen_index: i,\n }))\n }\n return dataWithKey?.flat()\n })\n }, [initialGeomData, keyAccessor])\n\n const positionKeys = useMemo(\n () => geomData.map(positionKeyAccessor).join(''),\n [geomData, positionKeyAccessor],\n )\n\n const [firstRender, setFirstRender] = useState(true)\n useEffect(() => {\n const timeout = setTimeout(() => setFirstRender(false), 0)\n return () => clearTimeout(timeout)\n }, [])\n\n useEffect(() => {\n if (firstRender && undefinedX.length > 0) {\n console.warn(\n `Ignoring ${undefinedX.length} points with missing x values.`,\n )\n }\n\n if (firstRender && undefinedY.length > 0) {\n console.warn(\n `Ignoring ${undefinedY.length} points with missing y values.`,\n )\n }\n }, [firstRender, undefinedX, undefinedY])\n\n const bottomPos = useMemo(\n () => (height && margin ? height - margin.bottom : undefined),\n [height, margin],\n )\n\n useEffect(() => {\n setTheme((prev) => ({\n ...prev,\n geoms: {\n ...prev.geoms,\n point: {\n fillOpacity: geomAttr.style?.fillOpacity || geomAttr.fillOpacity,\n stroke: geomAttr.stroke,\n strokeWidth: geomAttr.style?.strokeWidth || geomAttr.strokeWidth,\n strokeOpacity:\n geomAttr.style?.strokeOpacity || geomAttr.strokeOpacity,\n size: geomAes?.size,\n },\n },\n }))\n }, [attr, setTheme])\n\n const baseStyles: CSSProperties = {\n transition: 'fill-opacity 200ms',\n fillOpacity: geomAttr.fillOpacity,\n strokeOpacity: geomAttr.strokeOpacity,\n ...geomAttr.style,\n }\n\n const focusedStyles: CSSProperties = {\n ...baseStyles,\n ...focusedStyle,\n }\n\n const unfocusedStyles: CSSProperties = {\n ...baseStyles,\n fillOpacity: 0.2,\n strokeOpacity: 0.2,\n ...unfocusedStyle,\n }\n\n const fill = useMemo(\n () => (d: Datum) =>\n geomAttr.fill ||\n (geomAes?.fill && copiedScales?.fillScale\n ? (copiedScales.fillScale(geomAes.fill(d)) as string | undefined)\n : defaultFill),\n [geomAes, copiedScales, geomAttr, defaultFill],\n )\n\n const stroke = useMemo(\n () => (d: Datum) =>\n geomAttr.stroke ||\n (geomAes?.stroke && copiedScales?.strokeScale\n ? (copiedScales.strokeScale(geomAes.stroke(d) as any) as\n | string\n | undefined)\n : 'none'),\n [geomAes, copiedScales, geomAttr],\n )\n\n const radius = useMemo(() => {\n if (geomData && geomAes?.size && sizeRange) {\n const domain =\n sizeDomain && sizeDomain[0] && sizeDomain[1]\n ? sizeDomain\n : (extent(geomData, geomAes.size as () => number) as number[])\n return scaleSqrt()\n .domain(domain)\n .range(sizeRange as [number, number])\n .unknown([geomAttr.r])\n }\n return () => geomAttr.r\n }, [geomAttr, geomAes, geomData, sizeRange, sizeDomain])\n\n const x = useMemo(() => {\n if (scales?.xScale.bandwidth) {\n return (d: Datum) =>\n (scales?.xScale(geomAes?.x && geomAes.x(d)) || 0) +\n scales?.xScale.bandwidth() / 2 +\n 0.9\n }\n return (d: Datum) =>\n scales?.xScale && geomAes?.x && (scales.xScale(geomAes.x(d)) || 0)\n }, [scales, geomAes])\n\n const y = useMemo(() => {\n if (scales?.yScale.bandwidth) {\n return (d: Datum) =>\n (scales?.yScale(geomAes?.y && geomAes.y(d)) || 0) +\n scales?.yScale.bandwidth() / 2\n }\n return (d: Datum) =>\n scales?.yScale && geomAes?.y && (scales.yScale(geomAes.y(d)) || 0)\n }, [scales, geomAes])\n\n const groupRef = useRef<SVGGElement>(null)\n const points = groupRef.current?.getElementsByTagName('circle')\n\n const [shouldClip, setShouldClip] = useState(\n isClipped || isFixedX || isFixedY,\n )\n useEffect(() => {\n if (xZoomDomain?.current || yZoomDomain?.current) {\n setShouldClip(true)\n } else {\n const timeout = setTimeout(() => setShouldClip(isClipped), duration)\n return () => clearTimeout(timeout)\n }\n return undefined\n }, [isFixedX, isFixedY, xZoomDomain?.current, yZoomDomain?.current, duration])\n\n return (\n <>\n <g\n ref={groupRef}\n clipPath={shouldClip ? `url(#__gg_canvas_${id})` : undefined}\n >\n {!firstRender && isVisible && (\n <NodeGroup\n data={[...(geomData as any[])]}\n keyAccessor={(d) =>\n geomAes?.key\n ? keyAccessor(d)\n : `${keyAccessor(d)}-${d.gg_gen_index}`\n }\n start={(d) => ({\n cx: x(d),\n cy: entrance === Entrance.DATA ? y(d) : bottomPos,\n fill: fill(d),\n stroke: stroke(d),\n r: 0,\n fillOpacity: 0,\n strokeOpacity: 0,\n })}\n enter={(d) => ({\n cx: isAnimated ? [x(d)] : x(d),\n cy: isAnimated ? [y(d)] : y(d),\n r: isAnimated\n ? [\n attr?.r ??\n (geomAes?.size\n ? radius(geomAes.size(d) as number)\n : radius(d)),\n ]\n : attr?.r ??\n (geomAes?.size\n ? radius(geomAes.size(d) as number)\n : radius(d)),\n fill: isAnimated ? [fill(d)] : fill(d),\n stroke: isAnimated ? [stroke(d)] : stroke(d),\n fillOpacity: isAnimated\n ? [geomAttr.fillOpacity]\n : geomAttr.fillOpacity,\n strokeOpacity: isAnimated\n ? [geomAttr.strokeOpacity]\n : geomAttr.strokeOpacity,\n timing: { duration, ease: easeCubic },\n })}\n update={(d) => ({\n cx: isAnimated ? [x(d)] : x(d),\n cy: isAnimated ? [y(d)] : y(d),\n r: isAnimated\n ? [\n attr?.r ??\n (geomAes?.size\n ? radius(geomAes.size(d) as number)\n : radius(d)),\n ]\n : attr?.r ??\n (geomAes?.size\n ? radius(geomAes.size(d) as number)\n : radius(d)),\n fill: isAnimated ? [fill(d)] : fill(d),\n stroke: isAnimated ? [stroke(d)] : stroke(d),\n fillOpacity: isAnimated\n ? [geomAttr.fillOpacity]\n : geomAttr.fillOpacity,\n strokeOpacity: isAnimated\n ? [geomAttr.strokeOpacity]\n : geomAttr.strokeOpacity,\n timing: { duration, ease: easeCubic },\n })}\n leave={() => ({\n fill: isAnimated ? ['transparent'] : 'transparent',\n stroke: isAnimated ? ['transparent'] : 'transparent',\n cy: isAnimated ? [bottomPos] : bottomPos,\n timing: { duration, ease: easeCubic },\n })}\n interpolation={(begVal, endVal) => interpolate(begVal, endVal)}\n >\n {(nodes) => (\n <>\n {nodes.map(({ state, key }) => {\n let styles = {}\n if (validFocusedKeys.includes(key)) styles = focusedStyles\n if (\n validFocusedKeys?.length > 0 &&\n !validFocusedKeys.includes(key)\n )\n styles = unfocusedStyles\n return (\n <circle\n key={key}\n // eslint-disable-next-line react/jsx-props-no-spreading\n {...attr}\n r={state.r >= 0 ? state.r : geomAttr.r}\n fill={state.fill}\n stroke={state.stroke}\n cx={state.cx}\n cy={state.cy}\n fillOpacity={state.fillOpacity}\n strokeOpacity={state.strokeOpacity}\n style={{\n pointerEvents: 'none',\n ...baseStyles,\n ...styles,\n }}\n data-testid=\"__gg_geom_point\"\n />\n )\n })}\n </>\n )}\n </NodeGroup>\n )}\n </g>\n {(showTooltip || brushAction) && geomAes && (\n <>\n <EventArea\n data={geomData}\n showTooltip={showTooltip}\n brushAction={brushAction}\n aes={geomAes}\n x={x}\n y={y}\n onDatumFocus={onDatumFocus}\n onMouseOver={({ i }: { d: unknown; i: number[] }) => {\n const focusedIndexes = geomData.flatMap((gd, fi) =>\n validFocusedKeys.includes(keyAccessor(gd)) ? fi : [],\n )\n\n if (points) {\n focusNodes({\n nodes: points,\n focusedIndex: [...focusedIndexes, ...[i].flat()],\n focusedStyles,\n unfocusedStyles,\n })\n }\n }}\n onClick={\n onDatumSelection\n ? ({ d, i }: { d: any; i: number[] }) => {\n onDatumSelection(d, i)\n }\n : undefined\n }\n onMouseLeave={() => {\n if (points) {\n unfocusNodes({ nodes: points, baseStyles })\n if (validFocusedKeys && validFocusedKeys.length) {\n focusNodes({\n nodes: points,\n focusedIndex: geomData.flatMap((d, i) =>\n validFocusedKeys.includes(keyAccessor(d)) ? i : [],\n ),\n focusedStyles,\n unfocusedStyles,\n })\n }\n }\n\n if (onExit) onExit()\n }}\n positionKeys={positionKeys}\n />\n {showTooltip && <Tooltip aes={geomAes} group={group} />}\n </>\n )}\n </>\n )\n}\n\nGeomPoint.displayName = 'GeomPoint'\nexport { GeomPoint }\n","import { Aes, DataValue } from '@graphique/graphique'\n\nexport enum Entrance {\n DATA = 'data',\n BOTTOM = 'bottom',\n}\n\nexport enum SizeLabelDirection {\n L = 'left',\n R = 'right',\n}\n\nexport type GeomAes<Datum> = Omit<Aes<Datum>, 'x'> & {\n x?: DataValue<Datum>\n}\n","import React, { useMemo } from 'react'\nimport { useAtom } from 'jotai'\nimport {\n useGG,\n tooltipState,\n labelsState,\n TooltipContent,\n YTooltip,\n DataValue,\n TooltipProps,\n} from '@graphique/graphique'\nimport { DefaultTooltip } from './DefaultTooltip'\nimport { type GeomAes } from '../types'\n\ninterface Props<Datum> {\n aes: GeomAes<Datum>\n group?: DataValue<Datum>\n}\n\nexport const Tooltip = <Datum,>({ aes, group }: Props<Datum>) => {\n const { ggState } = useGG<Datum>() || {}\n const { id, scales, height, width } = ggState || { width: 0, height: 0 }\n\n const [\n { datum: tooltipDatum, position, xFormat, yFormat, measureFormat, content },\n ] = useAtom<TooltipProps<Datum>>(tooltipState)\n\n const [{ x: xLab, y: yLab }] = useAtom(labelsState)\n\n const datum = useMemo(() => tooltipDatum && tooltipDatum[0], [tooltipDatum])\n\n const label = useMemo(() => {\n const labelResolution = {\n given: datum && aes?.label && aes.label(datum),\n keyed: datum && aes?.key && aes.key(datum),\n }\n\n return labelResolution?.given || labelResolution?.keyed\n }, [aes, datum])\n\n const xScale: any = scales?.xScale\n const yScale: any = scales?.yScale\n\n const xAdj = useMemo(\n () => (scales?.xScale.bandwidth ? scales?.xScale.bandwidth() / 2 : 0),\n [scales],\n )\n const yAdj = useMemo(\n () => (scales?.yScale?.bandwidth ? scales.yScale.bandwidth() / 2 : 0),\n [scales],\n )\n\n const thisGroup = useMemo(\n () => datum && group && group(datum),\n [datum, group],\n )\n\n const tooltipContents: TooltipContent<Datum>[] = [\n {\n x: datum && aes?.x && xScale && xScale(aes.x(datum)),\n y: datum && aes?.y && yScale && yScale(aes.y(datum)),\n xLab,\n yLab,\n formattedX:\n datum &&\n aes?.x &&\n ((xFormat ? xFormat(aes.x(datum)) : aes.x(datum)) as string),\n formattedY:\n datum &&\n aes?.y &&\n ((yFormat ? yFormat(aes.y(datum)) : aes.y(datum)) as string),\n group: thisGroup,\n label,\n formattedMeasure:\n measureFormat &&\n (label || String(thisGroup)) &&\n measureFormat(label || thisGroup),\n datum: tooltipDatum,\n containerWidth: width,\n },\n ]\n\n const tooltipValue = content\n ? datum && <div>{content(tooltipContents)}</div>\n : datum && <DefaultTooltip data={tooltipContents} />\n\n const shouldShow =\n datum &&\n tooltipContents[0].x !== undefined &&\n tooltipContents[0].y !== undefined\n\n return shouldShow ? (\n <div>\n <YTooltip\n id={id as string}\n left={(tooltipContents[0].x || 0) + xAdj}\n top={\n position === 'data'\n ? -(height - (tooltipContents[0].y || 0) - yAdj)\n : -height\n }\n value={tooltipValue}\n />\n </div>\n ) : null\n}\n","import React, { useState, useEffect } from 'react'\nimport { useAtom } from 'jotai'\nimport {\n labelsState,\n TooltipContent,\n TooltipContainer,\n formatMissing,\n themeState,\n nodeToString,\n} from '@graphique/graphique'\n\ninterface Props<Datum> {\n data: TooltipContent<Datum>[]\n}\n\nexport const DefaultTooltip = <Datum,>({ data }: Props<Datum>) => {\n const [{ x: xLab, y: yLab }] = useAtom(labelsState)\n const [{ tooltip }] = useAtom(themeState)\n\n const [yLabel, setYLabel] = useState('')\n useEffect(() => {\n const timeout = setTimeout(() => setYLabel(nodeToString(yLab)))\n\n return () => clearTimeout(timeout)\n }, [yLab])\n\n return data ? (\n <TooltipContainer>\n {data.map((d) => {\n const formattedGroup = formatMissing(d.group)\n return (\n <div key={`group-tooltip-${d.label || formattedGroup}`}>\n <div\n style={{\n marginTop: 4,\n marginBottom: 4,\n }}\n >\n {(d.label || d.group !== '__group') && (\n <>\n {d.mark}\n <div\n style={{\n display: 'flex',\n alignItems: 'flex-end',\n fontWeight: 500,\n }}\n >\n <div style={{ marginBottom: 4 }}>\n <span\n style={{\n fontSize:\n tooltip?.groupLabel?.fontSize ||\n tooltip?.font?.size,\n }}\n >\n {d.formattedMeasure || formattedGroup}\n </span>\n </div>\n </div>\n </>\n )}\n <div style={{ display: 'flex', marginBottom: 2 }}>\n {xLab && (\n <div\n style={{\n fontSize:\n tooltip?.xLabel?.fontSize || tooltip?.font?.size,\n }}\n >\n {`${xLab}:`}\n </div>\n )}\n <div\n style={{\n marginLeft: 1,\n fontWeight: 500,\n fontSize:\n tooltip?.xLabel?.fontSize ||\n (tooltip?.font?.size || 12) + 1,\n }}\n >\n {d.formattedX}\n </div>\n </div>\n <div style={{ display: 'flex' }}>\n {yLabel && (\n <div\n style={{\n fontSize:\n tooltip?.yLabel?.fontSize || tooltip?.font?.size,\n }}\n >\n {`${yLabel}:`}\n </div>\n )}\n <div\n style={{\n marginLeft: 1,\n fontWeight: 500,\n fontSize:\n tooltip?.yLabel?.fontSize ||\n (tooltip?.font?.size || 12) + 1,\n }}\n >\n {d.formattedY}\n </div>\n </div>\n </div>\n </div>\n )\n })}\n </TooltipContainer>\n ) : null\n}\n","import React, { CSSProperties } from 'react'\nimport {\n useGG,\n themeState,\n IScale,\n LegendOrientation,\n} from '@graphique/graphique'\nimport { useAtom } from 'jotai'\nimport { CategoricalLegend } from './CategoricalLegend'\nimport { ColorBandLegend } from './ColorBandLegend'\n\nexport interface LegendProps {\n /** title of legend */\n title?: React.ReactNode\n /** determines vertical/horizontal orientation of categorical legend members (_default_: `LegendOrientation.V`) */\n orientation?: LegendOrientation\n /** function for formatting legend member labels (categorical) or tick labels (continuous) */\n format?: (v: string, i?: number) => string\n /** width of continuous legend in pixels (_default_: `320`) */\n width?: number\n /** approximate number of ticks for continuous legend (_default_: `width / 64`) */\n numTicks?: number\n /** callback called for click events on legend members */\n onSelection?: (v: string) => void\n /** additional styles passed to legend container */\n style?: CSSProperties\n}\n\nexport const Legend = <Datum,>({\n title,\n style,\n orientation = LegendOrientation.V,\n format,\n width = 320,\n numTicks = width / 64,\n onSelection,\n}: LegendProps) => {\n const { ggState } = useGG<Datum>() || {}\n const { copiedScales, copiedData, aes } = ggState || {}\n const [{ font }] = useAtom(themeState)\n\n const { groups } = copiedScales || {}\n\n // include aes?.strokeDasharray\n const hasAppearanceAes = aes?.fill || aes?.stroke\n\n const { fontSize } = { ...style }\n\n return hasAppearanceAes ? (\n <div\n style={{\n fontFamily: font?.family,\n ...style,\n }}\n >\n {title}\n {copiedData && copiedScales && groups ? (\n <CategoricalLegend\n legendData={copiedData}\n orientation={orientation}\n legendScales={copiedScales}\n labelFormat={format}\n fontSize={fontSize}\n onSelection={onSelection}\n />\n ) : (\n <ColorBandLegend\n scales={copiedScales as IScale<Datum>}\n tickFormat={format}\n numTicks={numTicks}\n fontSize={fontSize}\n width={width}\n />\n )}\n </div>\n ) : null\n}\n","import React, { useState, useEffect } from 'react'\nimport {\n useGG,\n themeState,\n fillScaleState,\n strokeScaleState,\n formatMissing,\n IScale,\n LegendOrientation,\n} from '@graphique/graphique'\nimport { useAtom } from 'jotai'\n\nexport interface CategoricalLegendProps<Datum> {\n legendData: Datum[]\n legendScales: IScale<Datum>\n orientation?: LegendOrientation\n labelFormat?: (v: string, i?: number) => string\n fontSize?: string | number\n onSelection?: (v: string) => void\n}\n\nexport const CategoricalLegend = <Datum,>({\n legendData,\n legendScales,\n orientation,\n labelFormat,\n fontSize = 12,\n onSelection,\n}: CategoricalLegendProps<Datum>) => {\n const [isFocused, setIsFocused] = useState<string[]>(\n legendScales.groups || [],\n )\n\n const [{ geoms, legend }] = useAtom(themeState)\n const [{ domain: fillDomain }] = useAtom(fillScaleState)\n const [{ domain: strokeDomain }] = useAtom(strokeScaleState)\n\n const legendGroups =\n ((fillDomain || strokeDomain) as string[]) || legendScales.groups\n\n const { ggState, updateData } = useGG<Datum>() || {}\n const { scales, data } = ggState || {}\n\n useEffect(() => {\n setIsFocused(scales?.groups || [])\n }, [scales, data])\n\n const getGroup: any = legendScales.groupAccessor\n ? legendScales.groupAccessor\n : () => legendScales.groups && legendScales.groups[0]\n\n const isHorizontal = orientation === 'horizontal'\n\n const toggleLegendGroup = (g: string) => {\n const prevFocused = isFocused\n let focusedGroups\n if (prevFocused.includes(g)) {\n if (prevFocused.length === 1) {\n focusedGroups = legendScales.groups as string[]\n } else {\n focusedGroups = prevFocused.filter((p) => p !== g)\n }\n } else {\n focusedGroups = [...prevFocused, g]\n }\n setIsFocused(focusedGroups)\n\n const includedGroups = Array.from(new Set(data?.map((d) => getGroup(d))))\n\n if (onSelection) {\n onSelection(g)\n }\n if (data && updateData) {\n let updatedData: Datum[]\n if (includedGroups.includes(g)) {\n if (includedGroups.length === 1) {\n updatedData = legendData\n } else {\n updatedData = data.filter((d) => getGroup(d) !== g)\n }\n } else {\n updatedData = legendData.filter(\n (d) => includedGroups.includes(getGroup(d)) || getGroup(d) === g,\n )\n }\n updateData(updatedData)\n }\n }\n\n return (\n <div\n style={{\n marginTop: 8,\n display: 'flex',\n flexDirection: !isHorizontal ? 'column' : 'row',\n flexWrap: 'wrap',\n alignItems: isHorizontal ? 'center' : undefined,\n }}\n >\n {geoms?.point?.fillOpacity &&\n legendGroups?.map((g: string, i, groups) => (\n <div\n key={g}\n style={{\n display: 'flex',\n alignItems: 'center',\n marginBottom: isHorizontal ? 6 : 2,\n }}\n >\n <div\n tabIndex={0}\n role=\"button\"\n style={{\n cursor: 'pointer',\n // scales?.fillScale?.domain().includes(g) ||\n // legendScales.groups?.includes(g)\n // ? \"pointer\"\n // : \"not-allowed\",\n marginRight: i < groups.length - 1 && isHorizontal ? 12 : 2,\n fontSize,\n opacity: isFocused.includes(g) ? 1 : 0.5,\n transition: 'opacity 200ms',\n display: 'flex',\n alignItems: 'center',\n }}\n onKeyPress={(e) => {\n if (['Enter', ' '].includes(e.key)) {\n toggleLegendGroup(g)\n }\n }}\n onClick={() => toggleLegendGroup(g)}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}\n >\n <svg width={12} height={12}>\n <circle\n r={4}\n cx={6}\n cy={6}\n fill={\n geoms?.point?.fill ||\n (legendScales.fillScale\n ? legendScales.fillScale(g)\n : 'none')\n }\n stroke={\n geoms?.point?.stroke ||\n (legendScales.strokeScale\n ? legendScales.strokeScale(g)\n : 'none')\n }\n strokeWidth={1.8}\n fillOpacity={\n isFocused.includes(g) ? geoms?.point?.fillOpacity : 0.5\n }\n strokeOpacity={\n isFocused.includes(g) ? geoms?.point?.strokeOpacity : 0.5\n }\n style={{\n transition: 'fill-opacity 200ms',\n }}\n />\n </svg>\n </div>\n <div\n style={{\n marginLeft: 4,\n fontSize,\n color: legend?.labelColor ?? 'currentcolor',\n }}\n >\n {labelFormat ? labelFormat(g, i) : formatMissing(g)}\n </div>\n </div>\n </div>\n ))}\n </div>\n )\n}\n","import React, { useCallback, useEffect, useState, useRef } from 'react'\nimport { useAtom } from 'jotai'\nimport {\n themeState,\n fillScaleState,\n strokeScaleState,\n IScale,\n} from '@graphique/graphique'\nimport { interpolateRound } from 'd3-interpolate'\nimport { select } from 'd3-selection'\nimport { axisBottom } from 'd3-axis'\nimport { range, quantile } from 'd3-array'\nimport { transition } from 'd3-transition'\n\nexport interface ColorBandLegendProps<Datum> {\n scales: IScale<Datum>\n tickFormat?: (v: string, i?: number) => string\n width: number\n tickSize?: number\n height?: number\n margin?: {\n top?: number\n right?: number\n bottom?: number\n left?: number\n }\n numTicks: number\n fontSize?: number | string\n}\n\nexport const ColorBandLegend = <Datum,>({\n scales,\n tickFormat,\n width,\n tickSize = 6,\n height = 30 + tickSize,\n margin,\n numTicks,\n fontSize = 10,\n}: ColorBandLegendProps<Datum>) => {\n const legendRef = useRef<SVGSVGElement | null>(null)\n const canvasRef = useRef<HTMLCanvasElement | null>(null)\n const axisRef = useRef<SVGGElement | null>(null)\n const ticksRef = useRef<SVGGElement | null>(null)\n const imageRef = useRef<SVGImageElement | null>(null)\n const colorScale = scales?.fillScale || scales?.strokeScale\n const [{ geoms, font: themeFont, legend, animationDuration }] =\n useAtom(themeState)\n\n const [{ reverse: reverseFill }] = useAtom(fillScaleState)\n const [{ reverse: reverseStroke }] = useAtom(strokeScaleState)\n\n const [firstRender, setFirstRender] = useState(true)\n useEffect(() => {\n const timeout = setTimeout(() => setFirstRender(false), 0)\n return () => clearTimeout(timeout)\n }, [])\n\n const isReversed = reverseFill || reverseStroke\n\n const RAMP_N = 256\n\n const usedMargin = {\n top: 4,\n right: 0,\n bottom: 16 + tickSize,\n left: 0,\n ...margin,\n }\n const drawLegend = useCallback(\n (scale: any, font?: string) => {\n if (\n legendRef.current &&\n canvasRef.current &&\n axisRef.current &&\n ticksRef.current &&\n imageRef.current\n ) {\n const ramp = (canvas: HTMLCanvasElement, color: any, n: number) => {\n const context = canvas.getContext('2d')\n for (let i = 0; i < n; i += 1) {\n if (context && color) {\n context.fillStyle = color(i / (n - 1))\n context.fillRect(isReversed ? n - i : i, 0, 1, 1)\n }\n }\n return canvas\n }\n\n const duration = animationDuration ?? 1000\n\n let x: any\n let tickValues: any\n const tickAdjust = (g: any) =>\n g\n .selectAll('.tick line')\n .attr('y1', usedMargin.top + usedMargin.bottom - height)\n\n // let scaleType = \"unknown\"\n\n const canvas = select(canvasRef.current)\n const axis = select(axisRef.current)\n const ticks = select(ticksRef.current)\n const img = select(imageRef.current)\n\n const t = transition().duration(duration)\n\n if (scale?.interpolate) {\n // scaleType = \"continuous\"\n } else if (scale?.interpolator) {\n // scaleType = \"sequential\"\n x = Object.assign(\n scale\n .copy()\n .interpolator(\n interpolateRound(usedMargin.left, width - usedMargin.right),\n ),\n {\n range() {\n return [usedMargin.left, width - usedMargin.right]\n },\n },\n )\n\n img\n .attr('x', usedMargin.left)\n .attr('y', usedMargin.top)\n .attr('width', width - usedMargin.left - usedMargin.right)\n .attr('height', height - usedMargin.top - usedMargin.bottom)\n .attr('preserveAspectRatio', 'none')\n .attr(\n 'xlink:href',\n ramp(\n canvasRef.current as HTMLCanvasElement,\n scale.interpolator(),\n RAMP_N,\n ).toDataURL(),\n )\n\n if (firstRender) {\n img\n .style('opacity', 0)\n .transition(t)\n .style(\n 'opacity',\n ((scales?.fillScale && geoms?.point?.fillOpacity) ||\n (scales?.strokeScale && geoms?.point?.strokeOpacity) ||\n undefined) as number | string,\n )\n }\n\n if (!x.ticks) {\n if (tickValues === undefined) {\n const n = Math.round(numTicks + 1)\n tickValues = range(n).map((i: number) =>\n quantile(scale.domain(), i / (n - 1)),\n )\n }\n }\n\n canvas.remove()\n }\n\n if (isReversed) {\n x.domain(x.domain().reverse())\n }\n\n axis\n .attr('transform', `translate(0,${height - usedMargin.bottom})`)\n .transition(t)\n .call(\n axisBottom(x)\n .ticks(\n numTicks,\n typeof tickFormat === 'string' ? tickFormat : undefined,\n )\n .tickFormat(\n typeof tickFormat === 'function'\n ? (tickFormat as any)\n : undefined,\n )\n .tickSize(tickSize)\n .tickValues(tickValues),\n )\n\n axis\n .call((g) => g.select('.domain').remove())\n .selectAll('line')\n .attr('stroke', legend?.tickColor || 'currentColor')\n .style('opacity', legend?.tickColor ? 1 : 0.85)\n\n axis\n .selectAll('.tick')\n .select('text')\n .style('font-family', font || 'sans-serif')\n .style('font-size', fontSize)\n .attr('fill', legend?.labelColor || 'currentColor')\n .style('opacity', legend?.labelColor ? 1 : 0.85)\n\n // ticks whose color isn't depenedent on currentColor\n ticks\n .attr('transform', `translate(0,${height - usedMargin.bottom})`)\n .transition(t)\n .call(\n axisBottom(x)\n .ticks(\n numTicks,\n typeof tickFormat === 'string' ? tickFormat : undefined,\n )\n .tickSize(1)\n .tickFormat(() => ''),\n )\n .selectAll('line')\n .attr('stroke', '#111')\n\n ticks\n .call((g) => g.select('.domain').remove())\n .call((g) => g.selectAll('.tick').select('text').remove())\n .call(tickAdjust)\n }\n },\n [\n width,\n height,\n numTicks,\n tickFormat,\n tickSize,\n usedMargin,\n legend,\n geoms,\n scales,\n fontSize,\n firstRender,\n isReversed,\n animationDuration,\n ],\n )\n\n useEffect(() => {\n drawLegend(colorScale, themeFont?.family)\n }, [themeFont, colorScale, drawLegend])\n\n return (\n <div>\n {themeFont?.family && (\n <svg\n ref={legendRef}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n style={{\n overflow: 'visible',\n display: 'block',\n }}\n >\n <image ref={imageRef} />\n <g ref={axisRef} />\n <g ref={ticksRef} />\n </svg>\n )}\n <canvas ref={canvasRef} width={RAMP_N} height={1} />\n </div>\n )\n}\n","import React, {\n CSSProperties,\n useEffect,\n useState,\n useMemo,\n useRef,\n} from 'react'\nimport { useAtom } from 'jotai'\nimport { select } from 'd3-selection'\nimport { extent, max } from 'd3-array'\nimport { useGG, radiusScaleState, themeState } from '@graphique/graphique'\nimport { scaleSqrt } from 'd3-scale'\nimport { transition } from 'd3-transition'\nimport { SizeLabelDirection } from '../types'\n\nexport interface SizeLegendProps {\n /** title of legend */\n title?: React.ReactNode\n /** which side of legend to draw labels (_default_: `SizeLabelDirection.R`) */\n labelDirection?: SizeLabelDirection\n /** number of circles to use as references in legend (_default_: `3`) */\n numCircles?: 2 | 3\n /** fixed breakpoints within size range for circle references (takes precedence over `numCircles`) */\n radiiVals?: [number, number] | [number, number, number]\n /** function for formatting labels */\n format?: (v: number, i?: number) => string\n /** width of legend in pixels (_default_: `120`) */\n width?: number\n /** additional styles passed to legend container */\n style?: CSSProperties\n}\n\nexport const SizeLegend = ({\n labelDirection = SizeLabelDirection.R,\n radiiVals,\n width = 120,\n numCircles = 3,\n format,\n style,\n title,\n}: SizeLegendProps) => {\n const [{ domain: sizeDomain, range: sizeRange }] =\n useAtom(radiusScaleState) || {}\n\n const [{ font, animationDuration, geoms }] = useAtom(themeState) || {}\n\n const { ggState } = useGG() || {}\n const { data } = ggState || {}\n\n const legendRef = useRef<SVGGElement | null>(null)\n\n const domain = useMemo(() => {\n if (sizeDomain && sizeDomain[0] && sizeDomain[1]) {\n return sizeDomain\n }\n if (data && geoms?.point?.size) {\n return extent(data, geoms.point.size) as [number, number]\n }\n return []\n }, [data, geoms, sizeDomain])\n\n const scale = useMemo(\n () =>\n scaleSqrt()\n .domain(domain)\n .range(sizeRange as [number, number]),\n [domain, sizeRange],\n )\n\n const tickVals = useMemo(() => {\n const ticks = scale.ticks()\n return (\n radiiVals ||\n (numCircles === 3\n ? [ticks[0], ticks[3], ticks[ticks.length - 1]]\n : (extent(ticks) as [number, number]))\n ).filter((v) => v >= domain[0]) as\n | [number, number, number]\n | [number, number]\n }, [radiiVals, numCircles, scale])\n\n const maxVal = useMemo(() => max(tickVals) || 0, [tickVals])\n\n const { fontSize, color } = { ...style }\n\n const [firstRender, setFirstRender] = useState(true)\n useEffect(() => {\n const timeout = setTimeout(() => setFirstRender(false), 5)\n return () => clearTimeout(timeout)\n }, [])\n\n useEffect(() => {\n const legendData = tickVals.map((v) => ({\n value: v,\n r: scale(v) < 0 ? 0 : scale(v),\n }))\n\n const duration = animationDuration ?? 1000\n const t = transition().duration(duration)\n\n // circles\n select(legendRef.current)\n .selectAll('circle')\n .attr('fill', 'transparent')\n .attr('stroke', 'currentColor')\n .style('opacity', 0.6)\n .attr('stroke-width', 1.5)\n .attr('stroke-dasharray', '0.05 4')\n .attr('stroke-linecap', 'round')\n .attr('cx', 0)\n .data(legendData, (_, i) => i)\n .join(\n (enter) =>\n enter.append('circle').attr('r', 0).attr('cy', scale(maxVal)),\n (update) =>\n update.call((toUpdate) =>\n toUpdate\n .transition(t)\n .attr('r', (d) => d.r)\n .attr('cy', (d) =>\n d.value === maxVal ? 0 : scale(maxVal) - d.r,\n ),\n ),\n )\n\n // tick marks\n select(legendRef.current)\n .selectAll('line')\n .attr('stroke', 'currentColor')\n .attr('x1', (d: any, i) =>\n labelDirection === SizeLabelDirection.R\n ? d.r / 2 + (i + 15) * 0.5 - (i + 1) * 2\n : -(d.r / 2 + (i + 15) * 0.5 - (i + 1) * 2),\n )\n .attr(\n 'x2',\n labelDirection === SizeLabelDirection.R\n ? scale(maxVal) + 15\n : -scale(maxVal) - 12,\n )\n .data(legendData, (_, i) => i)\n .join(\n (enter) =>\n enter\n .append('line')\n .attr('y1', scale(maxVal))\n .attr('y2', scale(maxVal))\n .style('opacity', 0),\n (update) =>\n update.call((toUpdate) =>\n toUpdate\n .transition(t)\n .attr('x1', (d, i) =>\n labelDirection === SizeLabelDirection.R\n ? d.r / 2 + (i + 15) * 0.5 - (i + 1) * 2\n : -(d.r / 2 + (i + 15) * 0.5 - (i + 1) * 2),\n )\n .attr('y1', (d) => scale(maxVal) - 2 * d.r + 3)\n .attr('y2', (d) => scale(maxVal) - 2 * d.r + 3)\n .style('opacity', 0.25),\n ),\n )\n\n // labels\n select(legendRef.current)\n .selectAll('text')\n .data(legendData, (_, i) => i)\n .attr(\n 'x',\n labelDirection === SizeLabelDirection.R\n ? scale(maxVal) + 18\n : -scale(maxVal) - 15,\n )\n .style('font-size', fontSize || 10)\n .attr('dominant-baseline', 'central')\n .attr(\n 'text-anchor',\n labelDirection === SizeLabelDirection.R ? 'start' : 'end',\n )\n .attr('fill', color || 'currentColor')\n .text((d: any, i) =>\n format ? format(d.value, i) : d.value.toLocaleString(),\n )\n .join(\n (enter) =>\n enter.append('text').attr('y', scale(maxVal)).style('opacity', 0),\n (update) =>\n update.call((toUpdate) =>\n toUpdate\n .transition(t)\n .attr(\n 'x',\n labelDirection === SizeLabelDirection.R\n ? scale(maxVal) + 18\n : -scale(maxVal) - 15,\n )\n .attr('y', (d) => scale(maxVal) - 2 * d.r + 3)\n .style('opacity', 0.85),\n ),\n )\n }, [\n firstRender,\n scale,\n tickVals,\n font,\n format,\n labelDirection,\n maxVal,\n fontSize,\n color,\n animationDuration,\n ])\n\n const xTranslation =\n labelDirection === SizeLabelDirection.R\n ? scale(maxVal) + 2\n : width - scale(maxVal) - 2\n\n const yTranslation = scale(maxVal) + 2\n\n return scale.domain()[0] && scale.domain()[1] ? (\n <div style={{ fontFamily: font?.family, ...style }}>\n {title}\n <div style={{ marginTop: 8 }}>\n <svg height={scale(maxVal) * 2 + 4} width={width}>\n <g\n style={{\n transform: `translate(${xTranslation}px, ${yTranslation}px)`,\n }}\n ref={legendRef}\n />\n </svg>\n </div>\n </div>\n ) : null\n}\n"],"mappings":"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