From 05b6af565d3f76c8dbe82191c0b162e598db68a3 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 29 Oct 2026 20:14:57 +0000 Subject: [PATCH 1/2] feat(solid): add a Solid 2 adapter at the solid2 subpath Adds `@tanstack/charts/solid/solid2` (and `@tanstack/solid-charts/solid2`), a Solid 2 port of the Solid adapter with the same props and types. The Solid 1 adapter at `@tanstack/charts/solid` is unchanged. The Solid 2 runtime and compiler live in the private `@charts-internal/solid-2-compat` package, mirroring react-18-compat, so the workspace toolchain stays on Solid 1. New test-solid2 and test-solid2-ssr targets, a second typecheck pass, and the adapter build cover the entry. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0117BoWMMJ6aTSA3MvtgJWG2 --- .changeset/solid-2-subpath.md | 6 + docs/framework/solid/adapter.md | 18 ++ docs/installation.md | 3 +- docs/reference/index.md | 1 + nx.json | 12 + .../docs/framework/solid/adapter.md | 18 ++ packages/charts-core/docs/installation.md | 3 +- packages/charts-core/docs/reference/index.md | 1 + packages/charts-core/package.json | 12 +- packages/charts-core/src/exports.test.ts | 2 + packages/charts-core/src/solid-solid2.ts | 1 + packages/solid-2-compat/package.json | 13 + packages/solid-charts/package.json | 17 +- .../src/solid2/Chart.ssr.test.tsx | 35 +++ .../solid-charts/src/solid2/Chart.test.tsx | 206 +++++++++++++ packages/solid-charts/src/solid2/Chart.tsx | 285 ++++++++++++++++++ packages/solid-charts/src/solid2/index.ts | 11 + packages/solid-charts/src/solid2/types.ts | 47 +++ packages/solid-charts/tsconfig.solid2.json | 21 ++ pnpm-lock.yaml | 255 ++++++++++++++++ project.json | 20 +- scripts/check-framework-adapters.mjs | 59 ++++ scripts/public-callback-contract.mjs | 4 + scripts/solid-2-compat.mjs | 46 +++ scripts/workspace-packages.test.mjs | 1 + tsconfig.json | 7 +- vitest.solid-ssr.config.ts | 3 +- vitest.solid2-ssr.config.ts | 19 ++ vitest.solid2.config.ts | 20 ++ 29 files changed, 1139 insertions(+), 7 deletions(-) create mode 100644 .changeset/solid-2-subpath.md create mode 100644 packages/charts-core/src/solid-solid2.ts create mode 100644 packages/solid-2-compat/package.json create mode 100644 packages/solid-charts/src/solid2/Chart.ssr.test.tsx create mode 100644 packages/solid-charts/src/solid2/Chart.test.tsx create mode 100644 packages/solid-charts/src/solid2/Chart.tsx create mode 100644 packages/solid-charts/src/solid2/index.ts create mode 100644 packages/solid-charts/src/solid2/types.ts create mode 100644 packages/solid-charts/tsconfig.solid2.json create mode 100644 scripts/solid-2-compat.mjs create mode 100644 vitest.solid2-ssr.config.ts create mode 100644 vitest.solid2.config.ts diff --git a/.changeset/solid-2-subpath.md b/.changeset/solid-2-subpath.md new file mode 100644 index 00000000..71889b89 --- /dev/null +++ b/.changeset/solid-2-subpath.md @@ -0,0 +1,6 @@ +--- +'@tanstack/charts': minor +'@tanstack/solid-charts': minor +--- + +Add a Solid 2 adapter at `@tanstack/charts/solid/solid2` (and `@tanstack/solid-charts/solid2`). It takes the same props and exports the same types as the Solid 1 adapter, which is unchanged at `@tanstack/charts/solid`. The `solid-js` peer now accepts `^1.8 || >=2.0.0-rc.19 <3.0.0`, with an optional `@solidjs/web` peer for the Solid 2 entry. diff --git a/docs/framework/solid/adapter.md b/docs/framework/solid/adapter.md index 76e83f63..59a0fc94 100644 --- a/docs/framework/solid/adapter.md +++ b/docs/framework/solid/adapter.md @@ -74,5 +74,23 @@ Read the context through `tooltip` instead of destructuring it so Solid tracks focused points and pinned state. The shared host owns focus, placement, portaling, and dismissal; Solid owns the returned component lifecycle. +## Solid 2 + +Solid 2 applications import the same component from the `solid2` subpath: + +```sh +pnpm add @tanstack/charts solid-js@next @solidjs/web@next +``` + +```tsx +import { Chart } from '@tanstack/charts/solid/solid2' +``` + +It takes the same props and exports the same types. `style` uses +`JSX.CSSProperties` from `@solidjs/web`, and `ChartTooltipBodyRenderContext` +types `defaultBody` as Solid 2's `Element`. Props flow through a split +`createEffect` and the host mounts in `onSettled`. `tooltip.dismiss` is a +stable function, so `onClick={tooltip.dismiss}` needs no tracking. + See the [`Chart` reference](./reference/chart.md), [SSR and hydration](../../guides/ssr-and-hydration.md), and [Chart Definition API](../../reference/chart-definitions.md). diff --git a/docs/installation.md b/docs/installation.md index aa39e614..4912e265 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -96,7 +96,8 @@ D3 dependency. | `@tanstack/charts/react-native` | React `^19.2.3`, React Native `^0.86.0` or `^0.88.0`, and `react-native-svg` `>=15.15.4 <16` | | `@tanstack/charts/preact` | Preact `>=10` | | `@tanstack/charts/vue` | Vue `>=3.5` | -| `@tanstack/charts/solid` | Solid `>=1.8` | +| `@tanstack/charts/solid` | Solid `^1.8` | +| `@tanstack/charts/solid/solid2` | Solid and `@solidjs/web` `>=2.0.0-rc.19 <3` | | `@tanstack/charts/svelte` | Svelte `^5.20.0` | | `@tanstack/charts/angular` | Angular core and platform browser `>=19` | | `@tanstack/charts/lit` | Lit `>=3.1.3` | diff --git a/docs/reference/index.md b/docs/reference/index.md index 20047bdc..8c7706a9 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -112,6 +112,7 @@ aggregate `/scales` export. | `@tanstack/charts/preact` | Preact `Chart` and chart prop, definition, point, and tooltip-body types | | `@tanstack/charts/vue` | Vue `Chart` and chart prop, definition, point, and tooltip-slot types | | `@tanstack/charts/solid` | Solid `Chart` and chart prop, definition, point, and tooltip-body types | +| `@tanstack/charts/solid/solid2` | Solid 2 `Chart` and the same chart prop, definition, point, and tooltip-body types | | `@tanstack/charts/svelte` | Svelte `Chart` and chart prop, definition, point, and tooltip-snippet types | | `@tanstack/charts/angular` | Angular `Chart`, tooltip-body directive, chart option, definition, point, and template-context types | | `@tanstack/charts/lit` | Lit `Chart`, `defineChartElement`, chart prop, definition, point, and tooltip-body types | diff --git a/nx.json b/nx.json index ba71a7df..0b890062 100644 --- a/nx.json +++ b/nx.json @@ -84,6 +84,8 @@ "{workspaceRoot}/packages/charts-scales/**/*", "{workspaceRoot}/packages/*-charts/**/*", "{workspaceRoot}/packages/react-18-compat/package.json", + "{workspaceRoot}/packages/solid-2-compat/package.json", + "{workspaceRoot}/scripts/solid-2-compat.mjs", "{workspaceRoot}/examples/charts-react-native/**/*", "{workspaceRoot}/examples/charts-expo/**/*", "!{workspaceRoot}/packages/*/dist/**/*", @@ -109,6 +111,8 @@ "{workspaceRoot}/vitest.solid*.config.ts", "{workspaceRoot}/packages/charts-core/**/*", "{workspaceRoot}/packages/solid-charts/**/*", + "{workspaceRoot}/packages/solid-2-compat/package.json", + "{workspaceRoot}/scripts/solid-2-compat.mjs", "!{workspaceRoot}/packages/*/dist/**/*", "!{workspaceRoot}/packages/**/CHANGELOG.md", "!{workspaceRoot}/packages/**/README.md", @@ -191,6 +195,14 @@ "cache": true, "inputs": ["testSolid"] }, + "test-solid2": { + "cache": true, + "inputs": ["testSolid"] + }, + "test-solid2-ssr": { + "cache": true, + "inputs": ["testSolid"] + }, "test-svelte": { "cache": true, "inputs": ["testSvelte"] diff --git a/packages/charts-core/docs/framework/solid/adapter.md b/packages/charts-core/docs/framework/solid/adapter.md index 76e83f63..59a0fc94 100644 --- a/packages/charts-core/docs/framework/solid/adapter.md +++ b/packages/charts-core/docs/framework/solid/adapter.md @@ -74,5 +74,23 @@ Read the context through `tooltip` instead of destructuring it so Solid tracks focused points and pinned state. The shared host owns focus, placement, portaling, and dismissal; Solid owns the returned component lifecycle. +## Solid 2 + +Solid 2 applications import the same component from the `solid2` subpath: + +```sh +pnpm add @tanstack/charts solid-js@next @solidjs/web@next +``` + +```tsx +import { Chart } from '@tanstack/charts/solid/solid2' +``` + +It takes the same props and exports the same types. `style` uses +`JSX.CSSProperties` from `@solidjs/web`, and `ChartTooltipBodyRenderContext` +types `defaultBody` as Solid 2's `Element`. Props flow through a split +`createEffect` and the host mounts in `onSettled`. `tooltip.dismiss` is a +stable function, so `onClick={tooltip.dismiss}` needs no tracking. + See the [`Chart` reference](./reference/chart.md), [SSR and hydration](../../guides/ssr-and-hydration.md), and [Chart Definition API](../../reference/chart-definitions.md). diff --git a/packages/charts-core/docs/installation.md b/packages/charts-core/docs/installation.md index aa39e614..4912e265 100644 --- a/packages/charts-core/docs/installation.md +++ b/packages/charts-core/docs/installation.md @@ -96,7 +96,8 @@ D3 dependency. | `@tanstack/charts/react-native` | React `^19.2.3`, React Native `^0.86.0` or `^0.88.0`, and `react-native-svg` `>=15.15.4 <16` | | `@tanstack/charts/preact` | Preact `>=10` | | `@tanstack/charts/vue` | Vue `>=3.5` | -| `@tanstack/charts/solid` | Solid `>=1.8` | +| `@tanstack/charts/solid` | Solid `^1.8` | +| `@tanstack/charts/solid/solid2` | Solid and `@solidjs/web` `>=2.0.0-rc.19 <3` | | `@tanstack/charts/svelte` | Svelte `^5.20.0` | | `@tanstack/charts/angular` | Angular core and platform browser `>=19` | | `@tanstack/charts/lit` | Lit `>=3.1.3` | diff --git a/packages/charts-core/docs/reference/index.md b/packages/charts-core/docs/reference/index.md index 20047bdc..8c7706a9 100644 --- a/packages/charts-core/docs/reference/index.md +++ b/packages/charts-core/docs/reference/index.md @@ -112,6 +112,7 @@ aggregate `/scales` export. | `@tanstack/charts/preact` | Preact `Chart` and chart prop, definition, point, and tooltip-body types | | `@tanstack/charts/vue` | Vue `Chart` and chart prop, definition, point, and tooltip-slot types | | `@tanstack/charts/solid` | Solid `Chart` and chart prop, definition, point, and tooltip-body types | +| `@tanstack/charts/solid/solid2` | Solid 2 `Chart` and the same chart prop, definition, point, and tooltip-body types | | `@tanstack/charts/svelte` | Svelte `Chart` and chart prop, definition, point, and tooltip-snippet types | | `@tanstack/charts/angular` | Angular `Chart`, tooltip-body directive, chart option, definition, point, and template-context types | | `@tanstack/charts/lit` | Lit `Chart`, `defineChartElement`, chart prop, definition, point, and tooltip-body types | diff --git a/packages/charts-core/package.json b/packages/charts-core/package.json index 74dcfef5..fae069bf 100644 --- a/packages/charts-core/package.json +++ b/packages/charts-core/package.json @@ -45,6 +45,7 @@ "peerDependencies": { "@angular/core": ">=19", "@angular/platform-browser": ">=19", + "@solidjs/web": ">=2.0.0-rc.19 <3.0.0", "alpinejs": ">=3.15", "lit": ">=3.1.3", "octane": "^0.1.13", @@ -53,7 +54,7 @@ "react-dom": "^18.0.0 || ^19.0.0", "react-native": "^0.86.0 || ^0.88.0", "react-native-svg": ">=15.15.4 <16", - "solid-js": ">=1.8", + "solid-js": "^1.8 || >=2.0.0-rc.19 <3.0.0", "svelte": "^5.20.0", "vue": ">=3.5" }, @@ -64,6 +65,9 @@ "@angular/platform-browser": { "optional": true }, + "@solidjs/web": { + "optional": true + }, "alpinejs": { "optional": true }, @@ -188,6 +192,7 @@ "./scales/point": "./src/scales-point.ts", "./scene": "./src/scene.ts", "./solid": "./src/solid.ts", + "./solid/solid2": "./src/solid-solid2.ts", "./svg": "./src/svg.ts", "./svg/renderer": "./src/svg-surface.ts", "./svg/resources": "./src/svg-resources.ts", @@ -582,6 +587,11 @@ "solid": "./dist/solid/index.js", "import": "./dist/solid/index.js" }, + "./solid/solid2": { + "types": "./dist/solid/solid2/index.d.ts", + "solid": "./dist/solid/solid2/index.js", + "import": "./dist/solid/solid2/index.js" + }, "./svg": { "types": "./dist/svg.d.ts", "import": "./dist/svg.js" diff --git a/packages/charts-core/src/exports.test.ts b/packages/charts-core/src/exports.test.ts index 978653ba..437fe77f 100644 --- a/packages/charts-core/src/exports.test.ts +++ b/packages/charts-core/src/exports.test.ts @@ -39,6 +39,8 @@ const specializedLoaderSpecifiers = new Set([ '@tanstack/charts/octane/core', '@tanstack/charts/react-native', '@tanstack/charts/react-native/tooltip', + // Solid 2 resolves from @charts-internal/solid-2-compat (vitest.solid2*). + '@tanstack/charts/solid/solid2', '@tanstack/charts/svelte', ]) diff --git a/packages/charts-core/src/solid-solid2.ts b/packages/charts-core/src/solid-solid2.ts new file mode 100644 index 00000000..a3b33c3b --- /dev/null +++ b/packages/charts-core/src/solid-solid2.ts @@ -0,0 +1 @@ +export * from '../../solid-charts/src/solid2/index' diff --git a/packages/solid-2-compat/package.json b/packages/solid-2-compat/package.json new file mode 100644 index 00000000..f7dc7e55 --- /dev/null +++ b/packages/solid-2-compat/package.json @@ -0,0 +1,13 @@ +{ + "name": "@charts-internal/solid-2-compat", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@solidjs/web": "2.0.0-rc.19", + "solid-js": "2.0.0-rc.19" + }, + "devDependencies": { + "@solidjs/vite-plugin": "3.0.0-next.52" + } +} diff --git a/packages/solid-charts/package.json b/packages/solid-charts/package.json index d3f2e8dc..95e83c1b 100644 --- a/packages/solid-charts/package.json +++ b/packages/solid-charts/package.json @@ -19,13 +19,23 @@ ".": { "solid": "./src/index.ts", "import": "./src/index.ts" + }, + "./solid2": { + "solid": "./src/solid2/index.ts", + "import": "./src/solid2/index.ts" } }, "dependencies": { "@tanstack/charts": "workspace:*" }, "peerDependencies": { - "solid-js": ">=1.8" + "@solidjs/web": ">=2.0.0-rc.19 <3.0.0", + "solid-js": "^1.8 || >=2.0.0-rc.19 <3.0.0" + }, + "peerDependenciesMeta": { + "@solidjs/web": { + "optional": true + } }, "devDependencies": { "solid-js": "^1.9.13", @@ -39,6 +49,11 @@ "types": "./dist/index.d.ts", "solid": "./dist/index.js", "import": "./dist/index.js" + }, + "./solid2": { + "types": "./dist/solid2/index.d.ts", + "solid": "./dist/solid2/index.js", + "import": "./dist/solid2/index.js" } } } diff --git a/packages/solid-charts/src/solid2/Chart.ssr.test.tsx b/packages/solid-charts/src/solid2/Chart.ssr.test.tsx new file mode 100644 index 00000000..8035a30f --- /dev/null +++ b/packages/solid-charts/src/solid2/Chart.ssr.test.tsx @@ -0,0 +1,35 @@ +/** @jsxImportSource @solidjs/web */ +import { renderToString } from '@solidjs/web' +import { describe, expect, it } from 'vitest' +import { defineChart, lineY } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { Chart } from './Chart' + +const rows = [ + { id: 'a', x: 0, y: 2 }, + { id: 'b', x: 1, y: 4 }, +] +const definition = defineChart({ + marks: [lineY(rows, { x: 'x', y: 'y', key: 'id' })], + scales: { + x: { scale: scaleLinear().domain([0, 1]) }, + y: { scale: scaleLinear().domain([0, 4]) }, + }, +}) + +describe('Solid 2 adapter SSR', () => { + it('server-renders complete SVG', () => { + const html = renderToString(() => ( + + )) + + expect(html).toContain('class="ts-chart-host"') + expect(html).toContain(' { + expectTypeOf(context.points).items.toMatchTypeOf<{ + datum: (typeof rows)[number] + xValue: number + yValue: number + }>() + expectTypeOf(context.content).toEqualTypeOf< + ChartTooltipContent | string + >() + expectTypeOf(context.defaultBody).toEqualTypeOf() + expectTypeOf(context.pinned).toEqualTypeOf() + expectTypeOf(context.dismiss).toEqualTypeOf<() => void>() + return context.defaultBody + }} + /> + ) + void typedTooltipBody +} + +describe('Solid 2 adapter', () => { + it('mounts and cleans up the shared host', () => { + const target = document.createElement('div') + let setLabel!: (label: string) => void + const dispose = render(() => { + const [label, set] = createSignal('Revenue') + setLabel = set + return ( + + ) + }, target) + + const svg = target.querySelector('svg') + expect(svg).not.toBeNull() + setLabel('Updated revenue') + flush() + expect(target.querySelector('svg')).toBe(svg) + expect(svg?.getAttribute('aria-label')).toBe('Updated revenue') + dispose() + expect(target.childElementCount).toBe(0) + }) + + it('composes and cleans up a pinned tooltip body', () => { + const tooltipDefinition = defineChart(definition, { + maxFocusDistance: 1_000, + tooltip: { + use: tooltip, + portal: tooltipPortal, + content: () => ({ + title: 'First', + color: '#2563eb', + rows: [ + { + label: 'Value', + value: '2', + color: '#2563eb', + }, + ], + }), + }, + }) + const target = document.createElement('div') + document.body.append(target) + const dispose = render( + () => ( + ( +
+ {context.defaultBody} + + {context.points[0]?.datum.id} + + {String(context.pinned)} + + +
+ )} + /> + ), + target, + ) + + const svg = target.querySelector('svg') + if (!svg) throw new Error('Expected an SVG chart') + vi.spyOn(svg, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + top: 0, + right: 480, + bottom: 260, + left: 0, + width: 480, + height: 260, + toJSON: () => ({}), + }) + + svg.dispatchEvent( + new MouseEvent('pointermove', { + bubbles: true, + clientX: 52, + clientY: 200, + }), + ) + flush() + + const portal = document.querySelector( + '[data-ts-chart-tooltip-portal]', + ) + const body = portal?.querySelector('.ts-chart-tooltip__body') + expect(portal).not.toBeNull() + expect(target.querySelector('[data-testid="rich-tooltip"]')).toBeNull() + expect(body?.querySelector('.ts-chart-tooltip__title')?.textContent).toBe( + 'First', + ) + expect(body?.querySelector('.ts-chart-tooltip__row')?.textContent).toBe( + 'Value2', + ) + expect( + body?.querySelector('[data-testid="tooltip-point"]')?.textContent, + ).toBe('a') + expect( + body?.querySelector('[data-testid="tooltip-pinned"]')?.textContent, + ).toBe('false') + const nestedSvg = body?.querySelector('svg[aria-label="Nested trend"]') + expect(nestedSvg).not.toBeNull() + expect(body?.hasAttribute('inert')).toBe(true) + expect(portal?.getAttribute('role')).toBe('status') + + svg.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + clientX: 52, + clientY: 200, + }), + ) + flush() + + expect( + body?.querySelector('[data-testid="tooltip-pinned"]')?.textContent, + ).toBe('true') + expect(portal?.dataset.sticky).toBe('true') + expect(body?.hasAttribute('inert')).toBe(false) + expect(portal?.getAttribute('role')).toBe('dialog') + expect(portal?.querySelector('.ts-chart-tooltip__body')).toBe(body) + expect(body?.querySelector('svg[aria-label="Nested trend"]')).toBe( + nestedSvg, + ) + + body + ?.querySelector('button') + ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + flush() + + expect(portal?.hidden).toBe(true) + expect(body?.querySelector('[data-testid="rich-tooltip"]')).toBeNull() + expect(body?.querySelector('svg[aria-label="Nested trend"]')).toBeNull() + + dispose() + expect(document.querySelector('[data-ts-chart-tooltip-portal]')).toBeNull() + target.remove() + }) +}) diff --git a/packages/solid-charts/src/solid2/Chart.tsx b/packages/solid-charts/src/solid2/Chart.tsx new file mode 100644 index 00000000..14bbbd44 --- /dev/null +++ b/packages/solid-charts/src/solid2/Chart.tsx @@ -0,0 +1,285 @@ +/** @jsxImportSource @solidjs/web */ +import { + Show, + createEffect, + createMemo, + createSignal, + createUniqueId, + onCleanup, + onSettled, + untrack, +} from 'solid-js' +import { Portal } from '@solidjs/web' +import type { Accessor, Element } from 'solid-js' +import { createChartRendererAdapter } from '@tanstack/charts/adapter/renderer' +import { renderChartSvg } from '@tanstack/charts/svg' +import { createSvgChartRenderer } from '@tanstack/charts/svg/renderer' +import type { + ChartRenderer, + ChartRendererHostOptions, + ChartRendererRenderContext, + ChartTooltipBodyTarget, + ChartTooltipContent, + ChartValue, +} from '@tanstack/charts' +import type { ChartProps, ChartTooltipBodyRenderContext } from './types' + +export function Chart< + TDatum, + TXValue extends ChartValue = ChartValue, + TYValue extends ChartValue = ChartValue, +>(props: ChartProps): Element { + const generatedId = createUniqueId() + const generatedPrefix = `ts-chart-${generatedId.replaceAll(/[^a-zA-Z0-9_-]/g, '')}` + const renderer = createMemo(() => + createSvgChartRenderer( + props.renderSvg ?? renderChartSvg, + ), + ) + const [tooltipBodyTarget, setTooltipBodyTarget] = + createSignal | null>( + null, + { ownedWrite: true }, + ) + const options = () => + toHostOptions( + props, + props.idPrefix ?? generatedPrefix, + renderer(), + setTooltipBodyTarget, + ) + const adapter = createChartRendererAdapter(untrack(options)) + const initialMarkup = adapter.prerender() + let container!: HTMLDivElement + + createEffect(options, (next) => { + adapter.update(next) + }) + onSettled(() => { + adapter.mount(container) + }) + onCleanup(() => adapter.destroy()) + + const aspectRatio = () => + typeof props.aspectRatio === 'number' && + Number.isFinite(props.aspectRatio) && + props.aspectRatio > 0 + ? props.aspectRatio + : undefined + + return ( + <> +
+
+
+ + {(renderTooltipBody) => ( + + {(target) => ( + + )} + + )} + + + ) +} + +function toHostOptions< + TDatum, + TXValue extends ChartValue, + TYValue extends ChartValue, +>( + props: ChartProps, + idPrefix: string, + renderer: ChartRenderer, + onTooltipBodyChange: ( + target: ChartTooltipBodyTarget | null, + ) => void, +): ChartRendererHostOptions { + const { + class: _class, + style: _style, + renderSvg: _renderSvg, + renderTooltipBody, + onRender, + ...options + } = props + const handleRender = onRender + ? (context: ChartRendererRenderContext): void => { + const element = + context.surface.defaultElement ?? context.surface.element + const SvgElement = + context.container.ownerDocument.defaultView?.SVGSVGElement + if (!SvgElement || !(element instanceof SvgElement)) { + throw new TypeError('Expected the SVG chart surface.') + } + onRender({ + container: context.container, + scene: context.scene, + surface: context.surface, + svg: element, + interaction: context.interaction, + }) + } + : undefined + + return { + ...options, + idPrefix, + renderer, + onRender: handleRender, + onTooltipBodyChange: renderTooltipBody ? onTooltipBodyChange : undefined, + } +} + +function TooltipBodyPortal< + TDatum, + TXValue extends ChartValue, + TYValue extends ChartValue, +>(props: { + render: NonNullable['renderTooltipBody']> + target: Accessor> +}): Element { + const defaultBody = + const context: ChartTooltipBodyRenderContext = { + get primaryPoint() { + return props.target().primaryPoint + }, + get points() { + return props.target().points + }, + get content() { + return props.target().content + }, + defaultBody, + get pinned() { + return props.target().pinned + }, + // A stable handler, so `onClick={context.dismiss}` needs no tracking. + dismiss: () => untrack(props.target).dismiss(), + } + const body = props.render(context) + + return {body} +} + +function DefaultTooltipBody(props: { + content: ChartTooltipContent | string +}): Element { + return ( + <> + {() => { + const content = props.content + return typeof content === 'string' ? ( + content + ) : ( + + ) + }} + + ) +} + +function StructuredTooltipBody(props: { + content: ChartTooltipContent +}): Element { + return ( + <> + {props.content.title ? ( +
+ {props.content.color ? ( + + ) : null} + {props.content.title} +
+ ) : null} + {props.content.rows.length ? ( + + ) : null} + + ) +} + +function TooltipSwatch(props: { color: string }): Element { + return ( +