diff --git a/.changeset/annotationpoint-smart-labels.md b/.changeset/annotationpoint-smart-labels.md new file mode 100644 index 000000000..fe5582cad --- /dev/null +++ b/.changeset/annotationpoint-smart-labels.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(AnnotationPoint): Support explicit `labelX`/`labelY` positioning, a `fontSize` prop, a `labelGap` for callout spacing, and a `labelPlacement="smart"` option that auto-orients the label and terminates its callout at the label edge. diff --git a/.changeset/geo-clipextent-viewport.md b/.changeset/geo-clipextent-viewport.md new file mode 100644 index 000000000..50aebe2b9 --- /dev/null +++ b/.changeset/geo-clipextent-viewport.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(GeoState): Support `clipExtent: true` to clip the projection to the chart dimensions (`[[0, 0], [width, height]]`), e.g. to trim the `Sphere` overflow under `geoMercator`. diff --git a/.changeset/labels-voronoi-layout.md b/.changeset/labels-voronoi-layout.md new file mode 100644 index 000000000..2136ce97d --- /dev/null +++ b/.changeset/labels-voronoi-layout.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Labels): Add `layout="voronoi"` to place each label towards Voronoi cell's centroid, `occlude` to drop overlapping labels and `links` to move labels to centroid with a leader line back to the point. diff --git a/.changeset/points-geo.md b/.changeset/points-geo.md new file mode 100644 index 000000000..d0e2f7f9b --- /dev/null +++ b/.changeset/points-geo.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Points|Labels): Project through the chart's geo projection when present, so `` (and ``) render on maps. diff --git a/.changeset/voronoi-cell-children.md b/.changeset/voronoi-cell-children.md new file mode 100644 index 000000000..d5592746e --- /dev/null +++ b/.changeset/voronoi-cell-children.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Voronoi): Add `children` snippet exposing per-cell geometry (`point`, `polygon`, `centroid`, `area`) for custom rendering (e.g. labels). diff --git a/docs/src/content/components/Labels.md b/docs/src/content/components/Labels.md index 374cc41ce..d3c25fe72 100644 --- a/docs/src/content/components/Labels.md +++ b/docs/src/content/components/Labels.md @@ -13,7 +13,7 @@ related: [] By default labels will be on the outside of bars, above for positive values and below for negative values -:example{ component="Bars" name="vertical-outside-labels-default" showCode } +:example{ component="Bars" name="vertical-outside-labels-default" } You can also use `placement="inside"` to place within the bars (near the value edge) @@ -51,6 +51,20 @@ Series end labels can be shown for multi-series line charts, and highlighted on :example{ component="LineChart" name="series-labels-hover" } +### Scatter plots + +Use `layout="voronoi"` to orient each label towards the open space of its Voronoi +cell, `occlude` to hide labels that would overlap a roomier-cell neighbor, and +`links` to move each label out into open space with a leader line back to the point +(d3-ring-note / smart-labels style) — no per-point placement setup required. + +:example{ name="voronoi" } + +It works on maps too — in a geo ``, `` and the labels project through +the projection, so the same props place airport labels in open space with leaders. + +:example{ name="voronoi-geo" } + ### Simplified charts Labels are also integrated in simplified charts via the `labels` prop diff --git a/docs/src/content/components/Voronoi.md b/docs/src/content/components/Voronoi.md index b4a83d0bb..7aa1b38d5 100644 --- a/docs/src/content/components/Voronoi.md +++ b/docs/src/content/components/Voronoi.md @@ -2,7 +2,7 @@ description: Interaction component which creates Voronoi diagrams to divide a plane according to the nearest points, aiding spatial analysis and visualization. category: interactions layers: [svg, canvas] -related: [TooltipContext] +related: [TooltipContext, Labels] --- ## Usage diff --git a/docs/src/content/guides/geo.md b/docs/src/content/guides/geo.md index 57dccc991..68e6489de 100644 --- a/docs/src/content/guides/geo.md +++ b/docs/src/content/guides/geo.md @@ -210,6 +210,12 @@ Restricts rendering to a rectangular pixel region: ``` +Pass `true` to clip to the chart dimensions (`[[0, 0], [width, height]]`) — e.g. to trim the `Sphere` overflow under `geoMercator`. Clips at the projection level, so it also applies to the canvas renderer and leaves non-projected marks (e.g. `Labels`) untouched: + +```svelte + +``` + ## Tooltips on maps Set `tooltip` on each `GeoPath` to wire up pointer events automatically. The default tooltipContext `manual` mode is used, where each shape calls `show`/`hide` on pointer enter/leave: @@ -366,7 +372,7 @@ The `geo` prop on `Chart` provides the primary projection context. Use the `GeoP | `fitGeojson` | `GeoPermissibleObjects` | GeoJSON to fit the projection to | | `fixedAspectRatio` | `number` | Fixed aspect ratio instead of responsive chart dimensions | | `clipAngle` | `number` | Angular extent of visible hemisphere (degrees) | -| `clipExtent` | `[[number, number], [number, number]]` | Rectangular pixel clipping region | +| `clipExtent` | `[[number, number], [number, number]] \| boolean` | Rectangular pixel clipping region (`true` = chart dimensions) | | `rotate` | `{ yaw, pitch, roll }` | Initial rotation in degrees | | `scale` | `number` | Manual projection scale (overrides fitGeojson scale) | | `translate` | `[number, number]` | Manual projection translate (overrides fitGeojson translate) | diff --git a/docs/src/examples/components/AnnotationPoint/link-callouts.svelte b/docs/src/examples/components/AnnotationPoint/link-callouts.svelte index 62a8a2354..2c5f23868 100644 --- a/docs/src/examples/components/AnnotationPoint/link-callouts.svelte +++ b/docs/src/examples/components/AnnotationPoint/link-callouts.svelte @@ -16,6 +16,7 @@ labelPlacement: 'top', labelXOffset: 20, labelYOffset: 40, + labelGap: 0, link: { type: 'beveled', radius: 15, sweep: 'vertical-horizontal' }, props: { circle: { class: 'stroke-secondary fill-secondary/10' }, diff --git a/docs/src/examples/components/AnnotationPoint/playground.svelte b/docs/src/examples/components/AnnotationPoint/playground.svelte index d5a5034f4..ba39812a5 100644 --- a/docs/src/examples/components/AnnotationPoint/playground.svelte +++ b/docs/src/examples/components/AnnotationPoint/playground.svelte @@ -20,7 +20,8 @@ 'right', 'bottom-left', 'bottom', - 'bottom-right' + 'bottom-right', + 'smart' ] as const; const anchorOptions = ['start', 'middle', 'end'].map((v) => ({ label: v, value: v })); @@ -40,10 +41,12 @@ let dataX = $state(80); let dataY = $state(4.25); - let placement: Placement = $state('bottom-right'); + let placement: Placement | 'smart' = $state('bottom-right'); let xOffset = $state(50); let yOffset = $state(50); - let radius = $state(100); + let radius = $state(60); + let fontSize = $state(16); + let labelGap = $state(2); let textAnchor: 'start' | 'middle' | 'end' = $state('middle'); let verticalAnchor: 'start' | 'middle' | 'end' = $state('start'); @@ -134,6 +137,20 @@ max={200} classes={{ root: 'flex-1 basis-40' }} /> + +
@@ -196,6 +213,8 @@ tooltipContext={false} > {#snippet aboveMarks({ context })} + {@const ringX = context.xScale(dataX)} + {@const ringY = context.yScale(dataY)} {#if showControls} - {@const ringX = context.xScale(dataX)} - {@const ringY = context.yScale(dataY)} {@const labelX = ringX + (radius * dirX) / dirMag + xOffset * signX} {@const labelY = ringY + (radius * dirY) / dirMag + yOffset * signY} diff --git a/docs/src/examples/components/Labels/voronoi-geo.svelte b/docs/src/examples/components/Labels/voronoi-geo.svelte new file mode 100644 index 000000000..e89a51a82 --- /dev/null +++ b/docs/src/examples/components/Labels/voronoi-geo.svelte @@ -0,0 +1,110 @@ + + + + +
+ + +
e.stopPropagation()} + role="none" + > + +
+
+ +
+ + +
+
+
+ + + + + {#each countries.features as country} + + {/each} + + + d.name.split(' ')[0]} + layout="voronoi" + links={useLinks ? { type: linkType, class: 'stroke-surface-content/40' } : false} + occlude={occludeLabels ? { padding: spacing } : false} + fontSize={9} + class="fill-surface-content stroke-surface-100 stroke-[3px] [paint-order:stroke] pointer-events-none" + /> + + diff --git a/docs/src/examples/components/Labels/voronoi.svelte b/docs/src/examples/components/Labels/voronoi.svelte new file mode 100644 index 000000000..76c4472bf --- /dev/null +++ b/docs/src/examples/components/Labels/voronoi.svelte @@ -0,0 +1,80 @@ + + +
+ +
e.stopPropagation()} + role="none" + > + +
+
+ +
+ + +
+
+ + + +
+ + + + {#if showVoronoi} + + {/if} + + d.i} + layout="voronoi" + links={useLinks ? { type: linkType, class: 'stroke-surface-content/40' } : false} + occlude={occludeLabels ? { padding: spacing } : false} + fontSize={10} + class="fill-surface-content pointer-events-none" + /> + + diff --git a/packages/layerchart/package.json b/packages/layerchart/package.json index fa9fcee4e..a2d940ec0 100644 --- a/packages/layerchart/package.json +++ b/packages/layerchart/package.json @@ -47,6 +47,7 @@ "@types/d3-interpolate": "^3.0.4", "@types/d3-interpolate-path": "^2.0.3", "@types/d3-path": "^3.1.1", + "@types/d3-polygon": "^3.0.2", "@types/d3-quadtree": "^3.0.6", "@types/d3-random": "^3.0.3", "@types/d3-sankey": "^0.12.5", @@ -93,6 +94,7 @@ "d3-interpolate": "^3.0.1", "d3-interpolate-path": "^2.3.0", "d3-path": "^3.1.0", + "d3-polygon": "^3.0.1", "d3-quadtree": "^3.0.1", "d3-random": "^3.0.1", "d3-sankey": "^0.12.3", diff --git a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte index a7f3276e8..1b902155a 100644 --- a/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte +++ b/packages/layerchart/src/lib/components/AnnotationPoint/AnnotationPoint.base.svelte @@ -13,14 +13,15 @@ Text: Component; }; - export type AnnotationPointBaseProps = AnnotationPointProps & - AnnotationPointBaseLayerComponents; + export type AnnotationPointBaseProps = AnnotationPointProps & AnnotationPointBaseLayerComponents; - - -{#if linkEndpoints} - -{/if} - -{#if label} - +{#if layerCtx === 'svg'} + +{:else if layerCtx === 'canvas'} + +{:else if layerCtx === 'html'} + {/if} - - diff --git a/packages/layerchart/src/lib/components/Labels/Labels.base.svelte b/packages/layerchart/src/lib/components/Labels/Labels.base.svelte index 4301a897c..cc0f1c246 100644 --- a/packages/layerchart/src/lib/components/Labels/Labels.base.svelte +++ b/packages/layerchart/src/lib/components/Labels/Labels.base.svelte @@ -6,26 +6,34 @@ Text: Component; Group: Component; Points: Component; + /** Leader lines for `layout="voronoi"` with `links`. Omitted on the HTML layer. */ + Link?: Component; }; export type LabelsBaseProps = LabelsProps & LabelsBaseLayerComponents; - + {#snippet children({ points }: { points: Point[] })} - {#each points as point, i (key(point.data, i))} - {@const baseProps = c.getTextProps(point, points, i)} - {@const textProps = extractLayerProps(baseProps, 'lc-labels-text')} - {#if childrenProp} - {@render childrenProp({ data: point, textProps })} - {:else} - - {/if} - {/each} + {#if layout === 'voronoi'} + {@const voronoiLabels = c.getVoronoiLabels(points)} + {#each points as point, i (key(point.data, i))} + {@const item = voronoiLabels[i]} + {#if item.visible} + {@const textProps = extractLayerProps(item.textProps, 'lc-labels-text')} + {#if childrenProp} + {@render childrenProp({ data: point, textProps, link: item.link })} + {:else} + {#if item.link && Link} + + {/if} + + {/if} + {/if} + {/each} + {:else} + {#each points as point, i (key(point.data, i))} + {@const baseProps = c.getTextProps(point, points, i)} + {@const textProps = extractLayerProps(baseProps, 'lc-labels-text')} + {#if childrenProp} + {@render childrenProp({ data: point, textProps })} + {:else} + + {/if} + {/each} + {/if} {/snippet} @@ -77,7 +130,8 @@ diff --git a/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte b/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte index 12755e7f9..1688e7ba8 100644 --- a/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte +++ b/packages/layerchart/src/lib/components/Labels/Labels.canvas.svelte @@ -7,10 +7,11 @@ import Text from '../Text/Text.canvas.svelte'; import Group from '../Group/Group.canvas.svelte'; import Points from '../Points/Points.canvas.svelte'; + import Link from '../Link/Link.canvas.svelte'; import type { LabelsProps } from './Labels.shared.svelte.js'; let props: LabelsProps = $props(); - + diff --git a/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts b/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts index 9aa81434e..f6a6a6cc7 100644 --- a/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts +++ b/packages/layerchart/src/lib/components/Labels/Labels.shared.svelte.ts @@ -1,14 +1,20 @@ import type { ComponentProps, Snippet } from 'svelte'; +import { Delaunay } from 'd3-delaunay'; +import { polygonArea, polygonCentroid } from 'd3-polygon'; import { format as formatValue, type FormatType, type FormatConfig } from '@layerstack/utils'; import type { Without } from '$lib/utils/types.js'; import { accessor, type Accessor } from '$lib/utils/common.js'; import { isScaleBand } from '$lib/utils/scales.svelte.js'; +import { occlude } from '$lib/utils/occlusion.js'; +import { getTextRect } from '$lib/utils/string.js'; import { getChartContext } from '$lib/contexts/chart.js'; import type { ChartState } from '$lib/states/chart.svelte.js'; import { createDimensionGetter } from '$lib/utils/rect.svelte.js'; -import type { TextProps } from '../Text/Text.shared.svelte.js'; +import { getPixelValue, type TextProps } from '../Text/Text.shared.svelte.js'; +import { getPointLabelLayout, getPointLabelRect } from '$lib/utils/labelPlacement.js'; import type { Point } from '../Points/Points.shared.svelte.js'; +import type Link from '../Link/Link.svelte'; export type LabelsPropsWithoutHTML = { /** Override data instead of using context */ @@ -25,13 +31,33 @@ export type LabelsPropsWithoutHTML = { seriesKey?: string; /** @default 'outside' */ placement?: 'inside' | 'outside' | 'middle' | 'center' | 'smart'; + /** + * Global positioning algorithm applied across all labels (distinct from the + * per-point `placement`). `'voronoi'` orients each label towards the open space + * of its cell — good for scatter plots and maps. + */ + layout?: 'voronoi'; + /** + * Hide labels that would overlap a higher-priority (roomier-cell) one, resolving + * the actual boxes rather than dropping by a proxy. Requires `layout`. Pass an + * object to tune the spacing — e.g. `{ padding: 8 }` for a sparser result. + */ + occlude?: boolean | { padding?: number }; + /** + * With `layout="voronoi"`, move each label out into its cell's open space and draw + * a leader line back to the point (d3-ring-note / smart-labels style). Pass `true` + * for a straight line, or an object to configure the ``. + */ + links?: boolean | Partial>; /** @default placement === 'center' || placement === 'middle' ? 0 : 4 */ offset?: number; /** The format of the label */ format?: FormatType | FormatConfig; /** @default (d, index) => index */ key?: (d: T, index: number) => any; - children?: Snippet<[{ data: Point; textProps: TextProps }]>; + children?: Snippet< + [{ data: Point; textProps: TextProps; link?: { x1: number; y1: number; x2: number; y2: number } | null }] + >; }; export type LabelsProps = LabelsPropsWithoutHTML & @@ -222,4 +248,127 @@ export class LabelsState { return result; } + + /** + * `layout="voronoi"`: orient each label towards the open space of its Voronoi cell. + * With `links`, move the label out to the cell centroid and draw a leader back to the + * point (using AnnotationPoint's `smart` geometry). When `occlude` is set, drop labels + * that would overlap a roomier-cell label. Returns per-point `{ textProps, link, visible }`, + * index-aligned to `points`. + */ + getVoronoiLabels(points: Point[]): Array<{ + textProps: TextProps; + link: { x1: number; y1: number; x2: number; y2: number } | null; + visible: boolean; + }> { + const props = this.#getProps(); + const offset = props.offset ?? 4; + const fontSize = getPixelValue(props.fontSize ?? 12); + const links = props.links != null && props.links !== false; + // Don't fling labels across the chart when a cell's centroid is distant (sparse regions) + const maxMove = this.ctx.width * 0.2; + + // Four candidate orientations (towards the cell centroid / open space) + const orient = [ + { textAnchor: 'start', dx: offset, dy: 0 }, + { textAnchor: 'middle', dx: 0, dy: offset + fontSize / 2 }, + { textAnchor: 'end', dx: -offset, dy: 0 }, + { textAnchor: 'middle', dx: 0, dy: -(offset + fontSize / 2) }, + ] as const; + + const voronoi = Delaunay.from( + points, + (p) => p.x, + (p) => p.y + ).voronoi([0, 0, this.ctx.width, this.ctx.height]); + + type Leader = { x1: number; y1: number; x2: number; y2: number } | null; + + const candidates = points.map((point, i) => { + const polygon = voronoi.cellPolygon(i) as [number, number][] | null; + const centroid = polygon ? polygonCentroid(polygon) : [point.x, point.y]; + const area = polygon ? Math.abs(polygonArea(polygon)) : 0; + + const displayValue = props.value + ? accessor(props.value)(point.data) + : isScaleBand(this.ctx.yScale) + ? point.xValue + : point.yValue; + const text = String(formatValue(displayValue, props.format as FormatType)); + const fill = typeof props.fill === 'function' ? accessor(props.fill)(point.data) : props.fill; + + if (links) { + // Move the label into the cell's open space (unless the centroid is too far) + const dist = Math.hypot(centroid[0] - point.x, centroid[1] - point.y); + const move = polygon != null && dist > 1e-6 && dist <= maxMove; + const opts = { + x: point.x, + y: point.y, + labelPlacement: 'smart' as const, + labelX: move ? centroid[0] : point.x, + labelY: move ? centroid[1] : point.y, + fontSize, + link: move, + }; + const layout = getPointLabelLayout(opts); + return { + point, + i, + area, + textProps: { + value: text, + fill, + x: layout.text.x, + y: layout.text.y, + textAnchor: layout.text.textAnchor, + verticalAnchor: layout.text.verticalAnchor, + } as TextProps, + box: getPointLabelRect(text, opts), + link: (move + ? { x1: point.x, y1: point.y, x2: layout.anchor.x, y2: layout.anchor.y } + : null) as Leader, + }; + } + + // Orient the label near the point, towards the open space (no leader) + const angle = + (Math.round((Math.atan2(centroid[1] - point.y, centroid[0] - point.x) / Math.PI) * 2) + 4) % + 4; + const o = orient[angle]; + return { + point, + i, + area, + textProps: { + value: text, + fill, + x: point.x, + y: point.y, + dx: o.dx, + dy: o.dy, + textAnchor: o.textAnchor, + verticalAnchor: 'middle', + } as TextProps, + box: getTextRect(text, point.x, point.y, { + dx: o.dx, + dy: o.dy, + textAnchor: o.textAnchor, + fontSize, + }), + link: null as Leader, + }; + }); + + const occludeOn = props.occlude != null && props.occlude !== false; + const padding = typeof props.occlude === 'object' ? (props.occlude.padding ?? 2) : 2; + const visible = occludeOn + ? new Set(occlude(candidates, (c) => c.box, { priority: (c) => c.area, padding }).map((c) => c.i)) + : null; + + return candidates.map((c) => ({ + textProps: c.textProps, + link: c.link, + visible: visible == null || visible.has(c.i), + })); + } } diff --git a/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte b/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte index 854d4a545..2446d1482 100644 --- a/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte +++ b/packages/layerchart/src/lib/components/Labels/Labels.svg.svelte @@ -7,10 +7,11 @@ import Text from '../Text/Text.svg.svelte'; import Group from '../Group/Group.svg.svelte'; import Points from '../Points/Points.svg.svelte'; + import Link from '../Link/Link.svg.svelte'; import type { LabelsProps } from './Labels.shared.svelte.js'; let props: LabelsProps = $props(); - + diff --git a/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts b/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts index 4c9ef5b10..9e1e5c261 100644 --- a/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts +++ b/packages/layerchart/src/lib/components/Points/Points.shared.svelte.ts @@ -5,7 +5,9 @@ import type { CommonStyleProps, Without } from '$lib/utils/types.js'; import { isScaleBand, type AnyScale } from '$lib/utils/scales.svelte.js'; import { accessor, type Accessor } from '$lib/utils/common.js'; import { getChartContext } from '$lib/contexts/chart.js'; +import { getGeoContext } from '$lib/contexts/geo.js'; import type { ChartState } from '$lib/states/chart.svelte.js'; +import type { GeoState } from '$lib/states/geo.svelte.js'; import type { CircleProps } from '../Circle/Circle.shared.svelte.js'; export type Point = { @@ -49,6 +51,7 @@ export type PointsProps = PointsPropsWithoutHTML & export class PointsState { #getProps: () => PointsProps = () => ({}) as PointsProps; ctx: ChartState = getChartContext(); + geo: GeoState = getGeoContext(); constructor(getProps: () => PointsProps) { this.#getProps = getProps; @@ -123,6 +126,21 @@ export class PointsState { #getPointObject(xVal: number, yVal: number, d: any, edgeIndex?: number): Point { const props = this.#getProps(); + + // In a geo chart, project the [x, y] pair directly (no band offsets / radial) + if (this.geo.projection) { + const [projX, projY] = this.geo.projection([xVal, yVal]) ?? [0, 0]; + return { + x: projX, + y: projY, + r: this.ctx.config.r ? this.ctx.rGet(d) : (props.r ?? 5), + xValue: xVal, + yValue: yVal, + data: d, + edgeIndex, + }; + } + const scaledX: number = this.ctx.xScale(xVal); const scaledY: number = this.ctx.yScale(yVal); diff --git a/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte b/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte index 5bc5aef01..f51ca06d4 100644 --- a/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte +++ b/packages/layerchart/src/lib/components/Voronoi/Voronoi.base.svelte @@ -14,8 +14,10 @@ {#if geo.projection} - {@const polygons = geoVoronoi().polygons(points)} - {#each polygons.features as feature} - {@const point = r ? geo.projection?.(feature.properties.sitecoordinates) : null} - - - onclick?.(e, { data: feature.properties.site.data, feature })} - onpointerenter={(e: PointerEvent) => - onpointerenter?.(e, { data: feature.properties.site.data, feature })} - onpointermove={(e: PointerEvent) => - onpointermove?.(e, { data: feature.properties.site.data, feature })} - onpointerdown={(e: PointerEvent) => - onpointerdown?.(e, { data: feature.properties.site.data, feature })} - ontouchmove={(e: TouchEvent) => { - e.preventDefault(); - }} - /> - - {/each} - {:else} - {@const voronoi = Delaunay.from(points).voronoi([0, 0, boundWidth, boundHeight])} + {#if geoPolygons} + {#each geoPolygons.features as feature} + {@const point = r ? geo.projection?.(feature.properties.sitecoordinates) : null} + + + onclick?.(e, { data: feature.properties.site.data, feature })} + onpointerenter={(e: PointerEvent) => + onpointerenter?.(e, { data: feature.properties.site.data, feature })} + onpointermove={(e: PointerEvent) => + onpointermove?.(e, { data: feature.properties.site.data, feature })} + onpointerdown={(e: PointerEvent) => + onpointerdown?.(e, { data: feature.properties.site.data, feature })} + ontouchmove={(e: TouchEvent) => { + e.preventDefault(); + }} + /> + + {/each} + {/if} + {:else if voronoi} {#each points as point, i} {@const pathData = voronoi.renderCell(i)} {#if pathData} @@ -139,6 +196,8 @@ {/if} {/each} {/if} + + {@render children?.({ cells })}