From 02509a71007762f979be6056d831294181619fd9 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 14 Aug 2026 13:38:34 -0600 Subject: [PATCH 1/2] Make catalog examples self-contained --- API-FRICTION.md | 16 +- .../conformance/DEFINITION-COVERAGE-AUDIT.md | 350 ++--- .../DEFINITION-COVERAGE-OVERVIEW.md | 348 ++--- benchmarks/conformance/README.md | 14 +- .../cases/01-line-gaps/example.tsx | 48 + .../cases/01-line-gaps/tanstack.ts | 35 +- .../02-multi-line-end-labels/example.tsx | 85 + .../02-multi-line-end-labels/tanstack.ts | 67 +- .../03-temperature-range-band/example.tsx | 64 + .../03-temperature-range-band/tanstack.ts | 62 +- .../cases/04-stacked-time-area/example.tsx | 79 + .../cases/04-stacked-time-area/tanstack.ts | 62 +- .../cases/100-radial-bars/example.tsx | 73 + .../cases/100-radial-bars/tanstack.test.ts | 2 +- .../cases/100-radial-bars/tanstack.ts | 56 +- .../cases/101-sunburst/example.tsx | 74 + .../cases/101-sunburst/tanstack.test.ts | 2 +- .../cases/101-sunburst/tanstack.ts | 55 +- .../cases/102-world-choropleth/example.tsx | 99 ++ .../cases/102-world-choropleth/plot.ts | 6 +- .../cases/102-world-choropleth/tanstack.ts | 78 +- .../cases/103-bubble-map/example.tsx | 87 ++ .../conformance/cases/103-bubble-map/plot.ts | 6 +- .../cases/103-bubble-map/tanstack.ts | 62 +- .../cases/104-orthographic-globe/example.tsx | 80 + .../cases/104-orthographic-globe/plot.ts | 2 +- .../cases/104-orthographic-globe/tanstack.ts | 63 +- .../cases/105-route-map/example.tsx | 83 + .../conformance/cases/105-route-map/plot.ts | 2 +- .../cases/105-route-map/tanstack.ts | 61 +- .../cases/106-polar-line/example.tsx | 87 ++ .../cases/106-polar-line/tanstack.ts | 72 +- .../cases/107-polar-scatter/example.tsx | 84 + .../cases/107-polar-scatter/tanstack.ts | 59 +- .../cases/108-country-choropleth/example.tsx | 98 ++ .../cases/108-country-choropleth/plot.ts | 6 +- .../cases/108-country-choropleth/tanstack.ts | 77 +- .../cases/109-us-state-choropleth/example.tsx | 87 ++ .../109-us-state-choropleth/tanstack.test.ts | 2 +- .../cases/109-us-state-choropleth/tanstack.ts | 104 +- .../109-us-state-choropleth/transform.ts | 2 +- .../cases/110-projection-gallery/example.tsx | 92 ++ .../cases/110-projection-gallery/plot.ts | 2 +- .../110-projection-gallery/tanstack.test.ts | 8 +- .../cases/110-projection-gallery/tanstack.ts | 82 +- .../cases/111-basic-sankey/example.tsx | 145 ++ .../cases/111-basic-sankey/tanstack.test.ts | 2 +- .../cases/111-basic-sankey/tanstack.ts | 122 +- .../cases/111-sankey-flow/example.tsx | 255 +++ .../cases/111-sankey-flow/tanstack.test.ts | 2 +- .../cases/111-sankey-flow/tanstack.ts | 230 +-- .../cases/112-motion-entrance/controls.tsx | 96 ++ .../cases/112-motion-entrance/example.tsx | 190 +++ .../112-motion-entrance/tanstack.test.ts | 21 +- .../cases/112-motion-entrance/tanstack.ts | 4 +- .../cases/112-motion-entrance/view.tsx | 58 +- .../cases/113-motion-updates/controls.tsx | 96 ++ .../cases/113-motion-updates/example.tsx | 343 ++++ .../cases/113-motion-updates/tanstack.test.ts | 2 +- .../cases/113-motion-updates/tanstack.ts | 136 +- .../cases/113-motion-updates/view.tsx | 4 +- .../cases/114-spring-line-motion/controls.tsx | 96 ++ .../cases/114-spring-line-motion/example.tsx | 199 +++ .../114-spring-line-motion/tanstack.test.ts | 4 +- .../cases/114-spring-line-motion/tanstack.ts | 60 +- .../cases/114-spring-line-motion/view.tsx | 4 +- .../cases/115-definition-motion/controls.tsx | 96 ++ .../cases/115-definition-motion/example.tsx | 216 +++ .../115-definition-motion/tanstack.test.ts | 4 +- .../cases/115-definition-motion/tanstack.ts | 91 +- .../cases/115-definition-motion/view.tsx | 2 +- .../cases/116-geometry-morph/controls.tsx | 96 ++ .../cases/116-geometry-morph/example.tsx | 428 +++++ .../cases/116-geometry-morph/tanstack.test.ts | 2 +- .../cases/116-geometry-morph/tanstack.ts | 284 +--- .../cases/116-geometry-morph/view.tsx | 2 +- .../cases/117-focus-cursor-motion/example.tsx | 201 +++ .../117-focus-cursor-motion/tanstack.test.ts | 2 +- .../cases/117-focus-cursor-motion/tanstack.ts | 124 +- .../cases/117-focus-cursor-motion/view.tsx | 2 +- .../118-token-usage-calendar/example.tsx | 139 ++ .../118-token-usage-calendar/tanstack.test.ts | 4 +- .../118-token-usage-calendar/tanstack.ts | 116 +- .../{chart.ts => example.tsx} | 63 +- .../119-stacked-bar-band-cursor/tanstack.ts | 4 +- .../119-stacked-bar-band-cursor/view.tsx | 2 +- .../{chart.ts => example.tsx} | 119 +- .../120-themed-interactive-area/styles.css | 78 + .../tanstack.test.ts | 2 +- .../120-themed-interactive-area/tanstack.ts | 23 +- .../cases/121-active-bar-dashboard/chart.ts | 127 -- .../121-active-bar-dashboard/example.tsx | 299 ++++ .../121-active-bar-dashboard/tanstack.test.ts | 2 +- .../121-active-bar-dashboard/tanstack.ts | 3 +- .../cases/121-active-bar-dashboard/view.tsx | 2 +- .../cases/122-premium-kpi-sparklines/chart.ts | 113 -- .../122-premium-kpi-sparklines/example.tsx | 386 +++++ .../122-premium-kpi-sparklines/tanstack.ts | 3 +- .../cases/122-premium-kpi-sparklines/view.tsx | 2 +- .../cases/123-active-donut-metric/chart.ts | 187 --- .../cases/123-active-donut-metric/example.tsx | 385 +++++ .../123-active-donut-metric/tanstack.test.ts | 2 +- .../cases/123-active-donut-metric/tanstack.ts | 3 +- .../cases/123-active-donut-metric/view.tsx | 4 +- .../cases/124-theme-palette-matrix/chart.ts | 103 -- .../124-theme-palette-matrix/example.tsx | 270 ++++ .../124-theme-palette-matrix/tanstack.test.ts | 2 +- .../124-theme-palette-matrix/tanstack.ts | 3 +- .../cases/124-theme-palette-matrix/view.tsx | 2 +- .../cases/125-sales-funnel/example.tsx | 77 + .../cases/125-sales-funnel/tanstack.ts | 59 +- .../cases/126-drillable-sunburst/example.tsx | 153 ++ .../126-drillable-sunburst/tanstack.test.ts | 2 +- .../cases/126-drillable-sunburst/tanstack.ts | 83 +- .../cases/127-shadcn-dashboard/dashboard.tsx | 73 +- .../{tanstack-view.tsx => example.tsx} | 87 +- .../127-shadcn-dashboard/recharts-view.tsx | 6 +- .../cases/127-shadcn-dashboard/tanstack.ts | 75 +- .../cases/128-shadcn-bar-multiple/case.json | 19 +- .../cases/128-shadcn-bar-multiple/data.ts | 14 - .../cases/128-shadcn-bar-multiple/example.tsx | 198 +++ .../128-shadcn-bar-multiple/recharts-view.tsx | 45 - .../cases/128-shadcn-bar-multiple/recharts.ts | 6 +- .../cases/128-shadcn-bar-multiple/styles.css | 461 ++++++ .../128-shadcn-bar-multiple/tanstack-view.tsx | 115 -- .../cases/128-shadcn-bar-multiple/tanstack.ts | 23 +- .../cases/128-shadcn-bar-multiple/view.tsx | 24 - .../cases/129-shadcn-pie-donut-text/case.json | 20 +- .../cases/129-shadcn-pie-donut-text/data.ts | 42 - .../129-shadcn-pie-donut-text/example.tsx | 213 +++ .../recharts-view.tsx | 60 - .../129-shadcn-pie-donut-text/recharts.ts | 6 +- .../129-shadcn-pie-donut-text/styles.css | 461 ++++++ .../tanstack-view.tsx | 131 -- .../129-shadcn-pie-donut-text/tanstack.ts | 23 +- .../cases/129-shadcn-pie-donut-text/view.tsx | 26 - .../cases/13-interval-timeline/example.tsx | 70 + .../cases/13-interval-timeline/tanstack.ts | 51 +- .../cases/130-shadcn-radar-multiple/case.json | 20 +- .../cases/130-shadcn-radar-multiple/data.ts | 31 - .../130-shadcn-radar-multiple/example.tsx | 223 +++ .../recharts-view.tsx | 44 - .../130-shadcn-radar-multiple/recharts.ts | 6 +- .../130-shadcn-radar-multiple/styles.css | 461 ++++++ .../tanstack-view.tsx | 138 -- .../130-shadcn-radar-multiple/tanstack.ts | 23 +- .../cases/130-shadcn-radar-multiple/view.tsx | 27 - .../cases/131-shadcn-radial-text/case.json | 24 +- .../cases/131-shadcn-radial-text/data.ts | 14 - .../cases/131-shadcn-radial-text/example.tsx | 264 ++++ .../131-shadcn-radial-text/recharts-view.tsx | 65 - .../cases/131-shadcn-radial-text/recharts.ts | 6 +- .../cases/131-shadcn-radial-text/styles.css | 461 ++++++ .../131-shadcn-radial-text/tanstack-view.tsx | 128 -- .../cases/131-shadcn-radial-text/tanstack.ts | 23 +- .../cases/131-shadcn-radial-text/view.tsx | 26 - .../132-shadcn-tooltip-advanced/case.json | 22 +- .../cases/132-shadcn-tooltip-advanced/data.ts | 33 - .../132-shadcn-tooltip-advanced/example.tsx | 260 ++++ .../recharts-view.tsx | 100 -- .../132-shadcn-tooltip-advanced/recharts.ts | 6 +- .../132-shadcn-tooltip-advanced/styles.css | 461 ++++++ .../tanstack-view.tsx | 155 -- .../132-shadcn-tooltip-advanced/tanstack.ts | 23 +- .../132-shadcn-tooltip-advanced/view.tsx | 20 - .../cases/133-shadcn-area-axes/example.tsx | 210 +++ .../cases/133-shadcn-area-axes/styles.css | 461 ++++++ .../cases/133-shadcn-area-axes/tanstack.ts | 21 +- .../cases/134-shadcn-area-default/example.tsx | 201 +++ .../cases/134-shadcn-area-default/styles.css | 461 ++++++ .../cases/134-shadcn-area-default/tanstack.ts | 21 +- .../135-shadcn-area-gradient/example.tsx | 221 +++ .../cases/135-shadcn-area-gradient/styles.css | 461 ++++++ .../135-shadcn-area-gradient/tanstack.ts | 21 +- .../cases/136-shadcn-area-icons/example.tsx | 230 +++ .../cases/136-shadcn-area-icons/styles.css | 461 ++++++ .../cases/136-shadcn-area-icons/tanstack.ts | 21 +- .../137-shadcn-area-interactive/example.tsx | 327 ++++ .../137-shadcn-area-interactive/styles.css | 461 ++++++ .../137-shadcn-area-interactive/tanstack.ts | 21 +- .../cases/138-shadcn-area-legend/example.tsx | 225 +++ .../cases/138-shadcn-area-legend/styles.css | 461 ++++++ .../cases/138-shadcn-area-legend/tanstack.ts | 21 +- .../cases/139-shadcn-area-linear/example.tsx | 201 +++ .../cases/139-shadcn-area-linear/styles.css | 461 ++++++ .../cases/139-shadcn-area-linear/tanstack.ts | 21 +- .../cases/14-error-bars/example.tsx | 110 ++ .../cases/14-error-bars/tanstack.test.ts | 2 +- .../cases/14-error-bars/tanstack.ts | 91 +- .../example.tsx | 225 +++ .../140-shadcn-area-stacked-expand/styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../cases/141-shadcn-area-stacked/example.tsx | 210 +++ .../cases/141-shadcn-area-stacked/styles.css | 461 ++++++ .../cases/141-shadcn-area-stacked/tanstack.ts | 21 +- .../cases/142-shadcn-area-step/example.tsx | 201 +++ .../cases/142-shadcn-area-step/styles.css | 461 ++++++ .../cases/142-shadcn-area-step/tanstack.ts | 21 +- .../cases/143-shadcn-bar-active/example.tsx | 198 +++ .../cases/143-shadcn-bar-active/styles.css | 461 ++++++ .../cases/143-shadcn-bar-active/tanstack.ts | 21 +- .../cases/144-shadcn-bar-default/example.tsx | 185 +++ .../cases/144-shadcn-bar-default/styles.css | 461 ++++++ .../cases/144-shadcn-bar-default/tanstack.ts | 21 +- .../145-shadcn-bar-horizontal/example.tsx | 174 +++ .../145-shadcn-bar-horizontal/styles.css | 461 ++++++ .../145-shadcn-bar-horizontal/tanstack.ts | 21 +- .../146-shadcn-bar-interactive/example.tsx | 239 +++ .../146-shadcn-bar-interactive/styles.css | 461 ++++++ .../146-shadcn-bar-interactive/tanstack.ts | 21 +- .../147-shadcn-bar-label-custom/example.tsx | 193 +++ .../147-shadcn-bar-label-custom/styles.css | 461 ++++++ .../147-shadcn-bar-label-custom/tanstack.ts | 21 +- .../cases/148-shadcn-bar-label/example.tsx | 197 +++ .../cases/148-shadcn-bar-label/styles.css | 461 ++++++ .../cases/148-shadcn-bar-label/tanstack.ts | 21 +- .../cases/149-shadcn-bar-mixed/example.tsx | 180 +++ .../cases/149-shadcn-bar-mixed/styles.css | 461 ++++++ .../cases/149-shadcn-bar-mixed/tanstack.ts | 21 +- .../conformance/cases/15-boxplot/example.tsx | 67 + .../cases/15-boxplot/tanstack.test.ts | 2 +- .../conformance/cases/15-boxplot/tanstack.ts | 57 +- .../cases/150-shadcn-bar-negative/example.tsx | 198 +++ .../cases/150-shadcn-bar-negative/styles.css | 461 ++++++ .../cases/150-shadcn-bar-negative/tanstack.ts | 21 +- .../cases/151-shadcn-bar-stacked/example.tsx | 204 +++ .../cases/151-shadcn-bar-stacked/styles.css | 461 ++++++ .../cases/151-shadcn-bar-stacked/tanstack.ts | 21 +- .../cases/152-shadcn-line-default/example.tsx | 212 +++ .../cases/152-shadcn-line-default/styles.css | 461 ++++++ .../cases/152-shadcn-line-default/tanstack.ts | 21 +- .../153-shadcn-line-dots-colors/example.tsx | 205 +++ .../153-shadcn-line-dots-colors/styles.css | 461 ++++++ .../153-shadcn-line-dots-colors/tanstack.ts | 21 +- .../154-shadcn-line-dots-custom/example.tsx | 234 +++ .../154-shadcn-line-dots-custom/styles.css | 461 ++++++ .../154-shadcn-line-dots-custom/tanstack.ts | 21 +- .../cases/155-shadcn-line-dots/example.tsx | 225 +++ .../cases/155-shadcn-line-dots/styles.css | 461 ++++++ .../cases/155-shadcn-line-dots/tanstack.ts | 21 +- .../156-shadcn-line-interactive/example.tsx | 255 +++ .../156-shadcn-line-interactive/styles.css | 461 ++++++ .../156-shadcn-line-interactive/tanstack.ts | 21 +- .../157-shadcn-line-label-custom/example.tsx | 207 +++ .../157-shadcn-line-label-custom/styles.css | 461 ++++++ .../157-shadcn-line-label-custom/tanstack.ts | 21 +- .../cases/158-shadcn-line-label/example.tsx | 225 +++ .../cases/158-shadcn-line-label/styles.css | 461 ++++++ .../cases/158-shadcn-line-label/tanstack.ts | 21 +- .../cases/159-shadcn-line-linear/example.tsx | 212 +++ .../cases/159-shadcn-line-linear/styles.css | 461 ++++++ .../cases/159-shadcn-line-linear/tanstack.ts | 21 +- .../conformance/cases/16-lollipop/example.tsx | 74 + .../conformance/cases/16-lollipop/tanstack.ts | 49 +- .../160-shadcn-line-multiple/example.tsx | 214 +++ .../cases/160-shadcn-line-multiple/styles.css | 461 ++++++ .../160-shadcn-line-multiple/tanstack.ts | 21 +- .../cases/161-shadcn-line-step/example.tsx | 212 +++ .../cases/161-shadcn-line-step/styles.css | 461 ++++++ .../cases/161-shadcn-line-step/tanstack.ts | 21 +- .../162-shadcn-pie-donut-active/example.tsx | 195 +++ .../162-shadcn-pie-donut-active/styles.css | 461 ++++++ .../162-shadcn-pie-donut-active/tanstack.ts | 21 +- .../cases/163-shadcn-pie-donut/example.tsx | 186 +++ .../cases/163-shadcn-pie-donut/styles.css | 461 ++++++ .../cases/163-shadcn-pie-donut/tanstack.ts | 21 +- .../164-shadcn-pie-interactive/example.tsx | 300 ++++ .../164-shadcn-pie-interactive/styles.css | 461 ++++++ .../164-shadcn-pie-interactive/tanstack.ts | 21 +- .../165-shadcn-pie-label-custom/example.tsx | 204 +++ .../165-shadcn-pie-label-custom/styles.css | 461 ++++++ .../165-shadcn-pie-label-custom/tanstack.ts | 21 +- .../166-shadcn-pie-label-list/example.tsx | 204 +++ .../166-shadcn-pie-label-list/styles.css | 461 ++++++ .../166-shadcn-pie-label-list/tanstack.ts | 21 +- .../cases/167-shadcn-pie-label/example.tsx | 214 +++ .../cases/167-shadcn-pie-label/styles.css | 461 ++++++ .../cases/167-shadcn-pie-label/tanstack.ts | 21 +- .../cases/168-shadcn-pie-legend/example.tsx | 185 +++ .../cases/168-shadcn-pie-legend/styles.css | 461 ++++++ .../cases/168-shadcn-pie-legend/tanstack.ts | 21 +- .../169-shadcn-pie-separator-none/example.tsx | 186 +++ .../169-shadcn-pie-separator-none/styles.css | 461 ++++++ .../169-shadcn-pie-separator-none/tanstack.ts | 21 +- .../conformance/cases/17-dumbbell/example.tsx | 75 + .../conformance/cases/17-dumbbell/tanstack.ts | 56 +- .../cases/170-shadcn-pie-simple/example.tsx | 186 +++ .../cases/170-shadcn-pie-simple/styles.css | 461 ++++++ .../cases/170-shadcn-pie-simple/tanstack.ts | 21 +- .../cases/171-shadcn-pie-stacked/example.tsx | 209 +++ .../cases/171-shadcn-pie-stacked/styles.css | 461 ++++++ .../cases/171-shadcn-pie-stacked/tanstack.ts | 21 +- .../172-shadcn-radar-default/example.tsx | 210 +++ .../cases/172-shadcn-radar-default/styles.css | 461 ++++++ .../172-shadcn-radar-default/tanstack.ts | 21 +- .../cases/173-shadcn-radar-dots/example.tsx | 220 +++ .../cases/173-shadcn-radar-dots/styles.css | 461 ++++++ .../cases/173-shadcn-radar-dots/tanstack.ts | 21 +- .../example.tsx | 210 +++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../example.tsx | 222 +++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../176-shadcn-radar-grid-circle/example.tsx | 220 +++ .../176-shadcn-radar-grid-circle/styles.css | 461 ++++++ .../176-shadcn-radar-grid-circle/tanstack.ts | 21 +- .../177-shadcn-radar-grid-custom/example.tsx | 207 +++ .../177-shadcn-radar-grid-custom/styles.css | 461 ++++++ .../177-shadcn-radar-grid-custom/tanstack.ts | 21 +- .../178-shadcn-radar-grid-fill/example.tsx | 210 +++ .../178-shadcn-radar-grid-fill/styles.css | 461 ++++++ .../178-shadcn-radar-grid-fill/tanstack.ts | 21 +- .../179-shadcn-radar-grid-none/example.tsx | 208 +++ .../179-shadcn-radar-grid-none/styles.css | 461 ++++++ .../179-shadcn-radar-grid-none/tanstack.ts | 21 +- .../cases/18-cumulative-histogram/example.tsx | 65 + .../cases/18-cumulative-histogram/tanstack.ts | 43 +- .../cases/180-shadcn-radar-icons/example.tsx | 243 +++ .../cases/180-shadcn-radar-icons/styles.css | 461 ++++++ .../cases/180-shadcn-radar-icons/tanstack.ts | 21 +- .../181-shadcn-radar-label-custom/example.tsx | 251 +++ .../181-shadcn-radar-label-custom/styles.css | 461 ++++++ .../181-shadcn-radar-label-custom/tanstack.ts | 21 +- .../cases/182-shadcn-radar-legend/example.tsx | 238 +++ .../cases/182-shadcn-radar-legend/styles.css | 461 ++++++ .../cases/182-shadcn-radar-legend/tanstack.ts | 21 +- .../183-shadcn-radar-lines-only/example.tsx | 223 +++ .../183-shadcn-radar-lines-only/styles.css | 461 ++++++ .../183-shadcn-radar-lines-only/tanstack.ts | 21 +- .../cases/184-shadcn-radar-radius/example.tsx | 216 +++ .../cases/184-shadcn-radar-radius/styles.css | 461 ++++++ .../cases/184-shadcn-radar-radius/tanstack.ts | 21 +- .../cases/185-shadcn-radial-grid/example.tsx | 207 +++ .../cases/185-shadcn-radial-grid/styles.css | 461 ++++++ .../cases/185-shadcn-radial-grid/tanstack.ts | 21 +- .../cases/186-shadcn-radial-label/example.tsx | 234 +++ .../cases/186-shadcn-radial-label/styles.css | 461 ++++++ .../cases/186-shadcn-radial-label/tanstack.ts | 21 +- .../cases/187-shadcn-radial-shape/example.tsx | 264 ++++ .../cases/187-shadcn-radial-shape/styles.css | 461 ++++++ .../cases/187-shadcn-radial-shape/tanstack.ts | 21 +- .../188-shadcn-radial-simple/example.tsx | 201 +++ .../cases/188-shadcn-radial-simple/styles.css | 461 ++++++ .../188-shadcn-radial-simple/tanstack.ts | 21 +- .../189-shadcn-radial-stacked/example.tsx | 249 +++ .../189-shadcn-radial-stacked/styles.css | 461 ++++++ .../189-shadcn-radial-stacked/tanstack.ts | 21 +- .../cases/19-moving-average-line/example.tsx | 61 + .../cases/19-moving-average-line/tanstack.ts | 61 +- .../190-shadcn-tooltip-default/example.tsx | 260 ++++ .../190-shadcn-tooltip-default/styles.css | 461 ++++++ .../190-shadcn-tooltip-default/tanstack.ts | 21 +- .../191-shadcn-tooltip-formatter/example.tsx | 260 ++++ .../191-shadcn-tooltip-formatter/styles.css | 461 ++++++ .../191-shadcn-tooltip-formatter/tanstack.ts | 21 +- .../192-shadcn-tooltip-icons/example.tsx | 280 ++++ .../cases/192-shadcn-tooltip-icons/styles.css | 461 ++++++ .../192-shadcn-tooltip-icons/tanstack.ts | 21 +- .../example.tsx | 260 ++++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../example.tsx | 260 ++++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../example.tsx | 260 ++++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../example.tsx | 260 ++++ .../styles.css | 461 ++++++ .../tanstack.ts | 21 +- .../197-shadcn-tooltip-label-none/example.tsx | 260 ++++ .../197-shadcn-tooltip-label-none/styles.css | 461 ++++++ .../197-shadcn-tooltip-label-none/tanstack.ts | 21 +- .../20-normalized-stacked-area/example.tsx | 72 + .../20-normalized-stacked-area/tanstack.ts | 58 +- .../cases/21-streamgraph/example.tsx | 71 + .../cases/21-streamgraph/tanstack.test.ts | 2 +- .../cases/21-streamgraph/tanstack.ts | 75 +- .../cases/22-bollinger-band/example.tsx | 89 ++ .../cases/22-bollinger-band/tanstack.test.ts | 2 +- .../cases/22-bollinger-band/tanstack.ts | 79 +- .../example.tsx | 89 ++ .../tanstack.ts | 68 +- .../cases/25-calendar-heatmap/example.tsx | 70 + .../cases/25-calendar-heatmap/tanstack.ts | 51 +- .../cases/26-diverging-likert/example.tsx | 72 + .../26-diverging-likert/tanstack.test.ts | 2 +- .../cases/26-diverging-likert/tanstack.ts | 66 +- .../cases/27-parallel-coordinates/example.tsx | 89 ++ .../27-parallel-coordinates/tanstack.test.ts | 2 +- .../cases/27-parallel-coordinates/tanstack.ts | 83 +- .../cases/28-candlestick/example.tsx | 85 + .../cases/28-candlestick/tanstack.ts | 66 +- .../cases/29-waterfall/example.tsx | 108 ++ .../cases/29-waterfall/tanstack.test.ts | 2 +- .../cases/29-waterfall/tanstack.ts | 100 +- .../cases/30-slopegraph/example.tsx | 117 ++ .../cases/30-slopegraph/tanstack.test.ts | 2 +- .../cases/30-slopegraph/tanstack.ts | 98 +- .../cases/31-linear-regression/example.tsx | 97 ++ .../31-linear-regression/tanstack.test.ts | 2 +- .../cases/31-linear-regression/tanstack.ts | 94 +- .../cases/32-change-arrows/example.tsx | 63 + .../cases/32-change-arrows/tanstack.ts | 56 +- .../cases/33-difference-chart/example.tsx | 72 + .../33-difference-chart/tanstack.test.ts | 2 +- .../cases/33-difference-chart/tanstack.ts | 52 +- .../cases/34-pointer-tooltip/example.tsx | 81 + .../cases/34-pointer-tooltip/tanstack.ts | 97 +- .../cases/35-grouped-tooltip/example.tsx | 82 + .../cases/35-grouped-tooltip/tanstack.ts | 120 +- .../cases/36-hierarchy-tree/example.tsx | 103 ++ .../cases/36-hierarchy-tree/tanstack.test.ts | 3 +- .../cases/36-hierarchy-tree/tanstack.ts | 82 +- .../cases/37-delaunay-network/example.tsx | 70 + .../cases/37-delaunay-network/tanstack.ts | 48 +- .../cases/38-contour-topography/example.tsx | 57 + .../38-contour-topography/tanstack.test.ts | 1 - .../cases/38-contour-topography/tanstack.ts | 38 +- .../cases/39-density-contours/example.tsx | 74 + .../39-density-contours/tanstack.test.ts | 1 - .../cases/39-density-contours/tanstack.ts | 52 +- .../40-force-directed-network/example.tsx | 119 ++ .../tanstack.test.ts | 3 +- .../40-force-directed-network/tanstack.ts | 99 +- .../cases/40-geojson-map/example.tsx | 59 + .../cases/40-geojson-map/tanstack.ts | 34 +- .../cases/41-waffle-unit-chart/example.tsx | 52 + .../cases/41-waffle-unit-chart/tanstack.ts | 54 +- .../cases/42-vector-field/example.tsx | 40 + .../cases/42-vector-field/tanstack.ts | 34 +- .../cases/43-hexbin-density/example.tsx | 85 + .../cases/43-hexbin-density/tanstack.ts | 66 +- .../cases/44-framed-scatter/example.tsx | 64 + .../cases/44-framed-scatter/tanstack.ts | 48 +- .../cases/50-empirical-cdf/example.tsx | 83 + .../cases/50-empirical-cdf/tanstack.test.ts | 2 +- .../cases/50-empirical-cdf/tanstack.ts | 68 +- .../51-faceted-distributions/example.tsx | 119 ++ .../51-faceted-distributions/tanstack.test.ts | 2 +- .../51-faceted-distributions/tanstack.ts | 100 +- .../cases/52-beeswarm-dodge/example.tsx | 68 + .../cases/52-beeswarm-dodge/tanstack.ts | 46 +- .../cases/53-log-scale-scatter/example.tsx | 72 + .../53-log-scale-scatter/tanstack.test.ts | 2 +- .../cases/53-log-scale-scatter/tanstack.ts | 70 +- .../cases/54-bump-ranking/example.tsx | 122 ++ .../cases/54-bump-ranking/tanstack.test.ts | 2 +- .../cases/54-bump-ranking/tanstack.ts | 104 +- .../cases/55-indexed-multi-line/example.tsx | 126 ++ .../55-indexed-multi-line/tanstack.test.ts | 2 +- .../cases/55-indexed-multi-line/tanstack.ts | 106 +- .../cases/56-connected-scatter/example.tsx | 108 ++ .../56-connected-scatter/tanstack.test.ts | 2 +- .../cases/56-connected-scatter/tanstack.ts | 89 +- .../example.tsx | 219 +++ .../tanstack.test.ts | 2 +- .../tanstack.ts | 213 +-- .../cases/58-select-extrema/example.tsx | 102 ++ .../cases/58-select-extrema/tanstack.test.ts | 2 +- .../cases/58-select-extrema/tanstack.ts | 105 +- .../cases/59-grouped-reducer-bars/example.tsx | 75 + .../cases/59-grouped-reducer-bars/tanstack.ts | 58 +- .../cases/60-lag-autocorrelation/example.tsx | 70 + .../cases/60-lag-autocorrelation/tanstack.ts | 63 +- .../cases/61-quantile-ribbon/example.tsx | 72 + .../cases/61-quantile-ribbon/tanstack.test.ts | 2 +- .../cases/61-quantile-ribbon/tanstack.ts | 82 +- .../cases/62-ridgeline-density/example.tsx | 105 ++ .../62-ridgeline-density/tanstack.test.ts | 2 +- .../cases/62-ridgeline-density/tanstack.ts | 86 +- .../cases/63-violin-distributions/example.tsx | 124 ++ .../63-violin-distributions/tanstack.test.ts | 2 +- .../cases/63-violin-distributions/tanstack.ts | 103 +- .../cases/64-marimekko-mosaic/example.tsx | 108 ++ .../64-marimekko-mosaic/tanstack.test.ts | 2 +- .../cases/64-marimekko-mosaic/tanstack.ts | 91 +- .../65-voronoi-nearest-tooltip/example.tsx | 94 ++ .../65-voronoi-nearest-tooltip/tanstack.ts | 121 +- .../cases/70-composed-chart/example.tsx | 85 + .../cases/70-composed-chart/tanstack.test.ts | 2 +- .../cases/70-composed-chart/tanstack.ts | 68 +- .../example.tsx | 80 + .../tanstack.test.ts | 2 +- .../tanstack.ts | 63 +- .../cases/72-recharts-mixed-bars/example.tsx | 84 + .../cases/72-recharts-mixed-bars/tanstack.ts | 65 +- .../cases/73-many-point-scatter/example.tsx | 62 + .../73-many-point-scatter/tanstack.test.ts | 2 +- .../cases/73-many-point-scatter/tanstack.ts | 59 +- .../cases/74-recharts-treemap/example.tsx | 61 + .../74-recharts-treemap/tanstack.test.ts | 2 +- .../cases/74-recharts-treemap/tanstack.ts | 39 +- .../conformance/cases/75-radar/example.tsx | 133 ++ .../cases/75-radar/tanstack.test.ts | 2 +- .../conformance/cases/75-radar/tanstack.ts | 114 +- .../conformance/cases/76-pie/example.tsx | 68 + .../conformance/cases/76-pie/tanstack.test.ts | 2 +- .../conformance/cases/76-pie/tanstack.ts | 47 +- .../conformance/cases/77-donut/example.tsx | 69 + .../cases/77-donut/tanstack.test.ts | 2 +- .../conformance/cases/77-donut/tanstack.ts | 48 +- .../conformance/cases/78-gauge/example.tsx | 73 + .../cases/78-gauge/tanstack.test.ts | 2 +- .../conformance/cases/78-gauge/tanstack.ts | 54 +- .../cases/80-echarts-axis-pointer/example.tsx | 154 ++ .../80-echarts-axis-pointer/tanstack.test.ts | 3 +- .../cases/80-echarts-axis-pointer/tanstack.ts | 91 +- .../example.tsx | 119 ++ .../tanstack.test.ts | 2 +- .../tanstack.ts | 3 +- .../81-recharts-interactive-legend/view.tsx | 91 +- .../82-chart-table-selection/example.tsx | 273 ++++ .../82-chart-table-selection/tanstack.test.ts | 21 +- .../82-chart-table-selection/tanstack.ts | 3 +- .../cases/82-chart-table-selection/view.tsx | 64 +- .../cases/83-focus-context-window/example.tsx | 321 ++++ .../83-focus-context-window/tanstack.test.ts | 22 +- .../cases/83-focus-context-window/tanstack.ts | 7 +- .../cases/83-focus-context-window/view.tsx | 125 +- .../example.tsx | 427 +++++ .../84-pinned-nested-chart-tooltip/model.ts | 3 +- .../tanstack.ts | 1 + .../84-pinned-nested-chart-tooltip/view.tsx | 268 +--- .../85-scrollable-resource-lanes/example.tsx | 377 +++++ .../85-scrollable-resource-lanes/tanstack.ts | 71 +- .../85-scrollable-resource-lanes/view.tsx | 2 +- .../example.tsx | 288 ++++ .../tanstack.test.ts | 4 +- .../tanstack.ts | 3 +- .../86-streaming-window-preservation/view.tsx | 62 +- .../example.tsx | 394 +++++ .../tanstack.test.ts | 2 +- .../tanstack.ts | 134 +- .../87-echarts-synchronized-cursors/view.tsx | 4 +- .../cases/88-echarts-free-cursor/example.tsx | 430 +++++ .../88-echarts-free-cursor/tanstack.test.ts | 2 +- .../cases/88-echarts-free-cursor/tanstack.ts | 199 +-- .../cases/88-echarts-free-cursor/view.tsx | 6 +- .../89-brush-range-selection/example.tsx | 231 +++ .../89-brush-range-selection/tanstack.test.ts | 2 +- .../89-brush-range-selection/tanstack.ts | 115 +- .../cases/89-brush-range-selection/view.tsx | 10 +- .../cases/90-zoomable-time-window/example.tsx | 268 ++++ .../90-zoomable-time-window/tanstack.test.ts | 2 +- .../cases/90-zoomable-time-window/tanstack.ts | 116 +- .../cases/90-zoomable-time-window/view.tsx | 6 +- .../91-timeline-playback-scrubber/example.tsx | 374 +++++ .../tanstack.test.ts | 2 +- .../91-timeline-playback-scrubber/tanstack.ts | 144 +- .../91-timeline-playback-scrubber/view.tsx | 6 +- .../cases/92-editable-event-range/example.tsx | 531 +++++++ .../92-editable-event-range/tanstack.test.ts | 2 +- .../cases/92-editable-event-range/tanstack.ts | 196 +-- .../cases/92-editable-event-range/view.tsx | 6 +- .../cases/93-labeled-pie/example.tsx | 91 ++ .../cases/93-labeled-pie/tanstack.test.ts | 2 +- .../cases/93-labeled-pie/tanstack.ts | 74 +- .../cases/94-center-donut/example.tsx | 87 ++ .../cases/94-center-donut/tanstack.test.ts | 2 +- .../cases/94-center-donut/tanstack.ts | 66 +- .../cases/95-rounded-donut/example.tsx | 71 + .../cases/95-rounded-donut/tanstack.test.ts | 2 +- .../cases/95-rounded-donut/tanstack.ts | 50 +- .../cases/96-nested-donut/example.tsx | 84 + .../cases/96-nested-donut/tanstack.test.ts | 2 +- .../cases/96-nested-donut/tanstack.ts | 65 +- .../conformance/cases/97-rose/example.tsx | 73 + .../cases/97-rose/tanstack.test.ts | 2 +- .../conformance/cases/97-rose/tanstack.ts | 54 +- .../cases/98-needle-gauge/example.tsx | 136 ++ .../cases/98-needle-gauge/tanstack.test.ts | 2 +- .../cases/98-needle-gauge/tanstack.ts | 115 +- .../cases/99-comparative-radar/example.tsx | 134 ++ .../99-comparative-radar/tanstack.test.ts | 2 +- .../cases/99-comparative-radar/tanstack.ts | 115 +- .../conformance/cases/bar-grouped/example.tsx | 84 + .../conformance/cases/bar-grouped/tanstack.ts | 65 +- .../cases/bar-horizontal-ranking/example.tsx | 60 + .../cases/bar-horizontal-ranking/tanstack.ts | 45 +- .../conformance/cases/bar-stacked/example.tsx | 70 + .../conformance/cases/bar-stacked/tanstack.ts | 51 +- .../cases/bar-vertical-sorted/example.tsx | 55 + .../cases/bar-vertical-sorted/tanstack.ts | 49 +- .../cases/facets-anscombe/example.tsx | 64 + .../cases/facets-anscombe/tanstack.ts | 48 +- .../cases/heatmap-labeled/example.tsx | 106 ++ .../cases/heatmap-labeled/tanstack.ts | 81 +- .../conformance/cases/histogram/example.tsx | 73 + .../conformance/cases/histogram/tanstack.ts | 54 +- .../cases/scatter-bubble/example.tsx | 63 + .../cases/scatter-bubble/tanstack.ts | 46 +- benchmarks/conformance/catalog-index.json | 1222 +++------------ .../definition-coverage-roadmap.json | 1381 ++++++++++------- .../definition-coverage-roadmap.test.ts | 2 +- benchmarks/conformance/native-catalog.ts | 2 +- .../conformance/previews/01-line-gaps.svg | 2 +- .../previews/03-temperature-range-band.svg | 2 +- .../previews/04-stacked-time-area.svg | 2 +- .../conformance/previews/106-polar-line.svg | 2 +- .../previews/109-us-state-choropleth.svg | 2 +- .../previews/110-projection-gallery.svg | 2 +- .../previews/128-shadcn-bar-multiple.svg | 2 +- .../previews/129-shadcn-pie-donut-text.svg | 2 +- .../previews/130-shadcn-radar-multiple.svg | 2 +- .../previews/131-shadcn-radial-text.svg | 2 +- .../previews/132-shadcn-tooltip-advanced.svg | 2 +- .../previews/19-moving-average-line.svg | 2 +- .../previews/20-normalized-stacked-area.svg | 2 +- .../conformance/previews/21-streamgraph.svg | 2 +- .../previews/22-bollinger-band.svg | 2 +- .../previews/31-linear-regression.svg | 2 +- .../previews/44-framed-scatter.svg | 2 +- .../conformance/previews/50-empirical-cdf.svg | 2 +- .../previews/53-log-scale-scatter.svg | 2 +- .../57-scatter-marginal-histograms.svg | 2 +- .../previews/58-select-extrema.svg | 2 +- .../previews/61-quantile-ribbon.svg | 2 +- .../previews/73-many-point-scatter.svg | 2 +- .../81-recharts-interactive-legend.svg | 2 +- benchmarks/conformance/previews/manifest.json | 102 +- .../conformance/previews/scatter-bubble.svg | 2 +- .../shared/fixtures/country-atlas.ts | 136 +- .../shared/fixtures/geography.test.ts | 6 +- .../shared/fixtures/simplify-geo.ts | 131 +- benchmarks/conformance/shared/mount.ts | 59 +- .../conformance/shared/shadcn-catalog-data.ts | 217 +-- .../shared/shadcn-catalog-tanstack.tsx | 2 +- .../shared/transforms/learning-poverty.ts | 97 +- nx.json | 4 + package.json | 2 + packages/charts-demo-data/package.json | 19 + .../charts-demo-data/src/country-atlas.ts | 135 ++ .../src/learning-poverty-geography.ts | 96 ++ .../src/shadcn-area-interactive-data.json | 0 packages/charts-demo-data/src/shadcn.ts | 216 +++ packages/charts-demo-data/src/simplify-geo.ts | 130 ++ project.json | 8 + scripts/catalog-definition-shapes.test.mjs | 88 +- scripts/catalog-index.mjs | 33 +- scripts/catalog-index.test.mjs | 12 +- scripts/check-catalog-examples.mjs | 215 +++ scripts/clean-catalog-examples.mjs | 28 + scripts/demo-data.test.mjs | 20 +- scripts/generate-shadcn-cases.mjs | 92 +- scripts/shadcn-example-source.mjs | 858 ++++++++++ scripts/typescript-source.mjs | 110 ++ 648 files changed, 69516 insertions(+), 13695 deletions(-) create mode 100644 benchmarks/conformance/cases/01-line-gaps/example.tsx create mode 100644 benchmarks/conformance/cases/02-multi-line-end-labels/example.tsx create mode 100644 benchmarks/conformance/cases/03-temperature-range-band/example.tsx create mode 100644 benchmarks/conformance/cases/04-stacked-time-area/example.tsx create mode 100644 benchmarks/conformance/cases/100-radial-bars/example.tsx create mode 100644 benchmarks/conformance/cases/101-sunburst/example.tsx create mode 100644 benchmarks/conformance/cases/102-world-choropleth/example.tsx create mode 100644 benchmarks/conformance/cases/103-bubble-map/example.tsx create mode 100644 benchmarks/conformance/cases/104-orthographic-globe/example.tsx create mode 100644 benchmarks/conformance/cases/105-route-map/example.tsx create mode 100644 benchmarks/conformance/cases/106-polar-line/example.tsx create mode 100644 benchmarks/conformance/cases/107-polar-scatter/example.tsx create mode 100644 benchmarks/conformance/cases/108-country-choropleth/example.tsx create mode 100644 benchmarks/conformance/cases/109-us-state-choropleth/example.tsx create mode 100644 benchmarks/conformance/cases/110-projection-gallery/example.tsx create mode 100644 benchmarks/conformance/cases/111-basic-sankey/example.tsx create mode 100644 benchmarks/conformance/cases/111-sankey-flow/example.tsx create mode 100644 benchmarks/conformance/cases/112-motion-entrance/controls.tsx create mode 100644 benchmarks/conformance/cases/112-motion-entrance/example.tsx create mode 100644 benchmarks/conformance/cases/113-motion-updates/controls.tsx create mode 100644 benchmarks/conformance/cases/113-motion-updates/example.tsx create mode 100644 benchmarks/conformance/cases/114-spring-line-motion/controls.tsx create mode 100644 benchmarks/conformance/cases/114-spring-line-motion/example.tsx create mode 100644 benchmarks/conformance/cases/115-definition-motion/controls.tsx create mode 100644 benchmarks/conformance/cases/115-definition-motion/example.tsx create mode 100644 benchmarks/conformance/cases/116-geometry-morph/controls.tsx create mode 100644 benchmarks/conformance/cases/116-geometry-morph/example.tsx create mode 100644 benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx create mode 100644 benchmarks/conformance/cases/118-token-usage-calendar/example.tsx rename benchmarks/conformance/cases/119-stacked-bar-band-cursor/{chart.ts => example.tsx} (62%) rename benchmarks/conformance/cases/120-themed-interactive-area/{chart.ts => example.tsx} (61%) create mode 100644 benchmarks/conformance/cases/120-themed-interactive-area/styles.css delete mode 100644 benchmarks/conformance/cases/121-active-bar-dashboard/chart.ts create mode 100644 benchmarks/conformance/cases/121-active-bar-dashboard/example.tsx delete mode 100644 benchmarks/conformance/cases/122-premium-kpi-sparklines/chart.ts create mode 100644 benchmarks/conformance/cases/122-premium-kpi-sparklines/example.tsx delete mode 100644 benchmarks/conformance/cases/123-active-donut-metric/chart.ts create mode 100644 benchmarks/conformance/cases/123-active-donut-metric/example.tsx delete mode 100644 benchmarks/conformance/cases/124-theme-palette-matrix/chart.ts create mode 100644 benchmarks/conformance/cases/124-theme-palette-matrix/example.tsx create mode 100644 benchmarks/conformance/cases/125-sales-funnel/example.tsx create mode 100644 benchmarks/conformance/cases/126-drillable-sunburst/example.tsx rename benchmarks/conformance/cases/127-shadcn-dashboard/{tanstack-view.tsx => example.tsx} (72%) delete mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/data.ts create mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx delete mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts-view.tsx create mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/styles.css delete mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack-view.tsx delete mode 100644 benchmarks/conformance/cases/128-shadcn-bar-multiple/view.tsx delete mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/data.ts create mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx delete mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts-view.tsx create mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/styles.css delete mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack-view.tsx delete mode 100644 benchmarks/conformance/cases/129-shadcn-pie-donut-text/view.tsx create mode 100644 benchmarks/conformance/cases/13-interval-timeline/example.tsx delete mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/data.ts create mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx delete mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts-view.tsx create mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/styles.css delete mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack-view.tsx delete mode 100644 benchmarks/conformance/cases/130-shadcn-radar-multiple/view.tsx delete mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/data.ts create mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx delete mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/recharts-view.tsx create mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/styles.css delete mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/tanstack-view.tsx delete mode 100644 benchmarks/conformance/cases/131-shadcn-radial-text/view.tsx delete mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/data.ts create mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx delete mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts-view.tsx create mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/styles.css delete mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack-view.tsx delete mode 100644 benchmarks/conformance/cases/132-shadcn-tooltip-advanced/view.tsx create mode 100644 benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx create mode 100644 benchmarks/conformance/cases/133-shadcn-area-axes/styles.css create mode 100644 benchmarks/conformance/cases/134-shadcn-area-default/example.tsx create mode 100644 benchmarks/conformance/cases/134-shadcn-area-default/styles.css create mode 100644 benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx create mode 100644 benchmarks/conformance/cases/135-shadcn-area-gradient/styles.css create mode 100644 benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx create mode 100644 benchmarks/conformance/cases/136-shadcn-area-icons/styles.css create mode 100644 benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx create mode 100644 benchmarks/conformance/cases/137-shadcn-area-interactive/styles.css create mode 100644 benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx create mode 100644 benchmarks/conformance/cases/138-shadcn-area-legend/styles.css create mode 100644 benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx create mode 100644 benchmarks/conformance/cases/139-shadcn-area-linear/styles.css create mode 100644 benchmarks/conformance/cases/14-error-bars/example.tsx create mode 100644 benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx create mode 100644 benchmarks/conformance/cases/140-shadcn-area-stacked-expand/styles.css create mode 100644 benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx create mode 100644 benchmarks/conformance/cases/141-shadcn-area-stacked/styles.css create mode 100644 benchmarks/conformance/cases/142-shadcn-area-step/example.tsx create mode 100644 benchmarks/conformance/cases/142-shadcn-area-step/styles.css create mode 100644 benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx create mode 100644 benchmarks/conformance/cases/143-shadcn-bar-active/styles.css create mode 100644 benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx create mode 100644 benchmarks/conformance/cases/144-shadcn-bar-default/styles.css create mode 100644 benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx create mode 100644 benchmarks/conformance/cases/145-shadcn-bar-horizontal/styles.css create mode 100644 benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx create mode 100644 benchmarks/conformance/cases/146-shadcn-bar-interactive/styles.css create mode 100644 benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx create mode 100644 benchmarks/conformance/cases/147-shadcn-bar-label-custom/styles.css create mode 100644 benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx create mode 100644 benchmarks/conformance/cases/148-shadcn-bar-label/styles.css create mode 100644 benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx create mode 100644 benchmarks/conformance/cases/149-shadcn-bar-mixed/styles.css create mode 100644 benchmarks/conformance/cases/15-boxplot/example.tsx create mode 100644 benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx create mode 100644 benchmarks/conformance/cases/150-shadcn-bar-negative/styles.css create mode 100644 benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx create mode 100644 benchmarks/conformance/cases/151-shadcn-bar-stacked/styles.css create mode 100644 benchmarks/conformance/cases/152-shadcn-line-default/example.tsx create mode 100644 benchmarks/conformance/cases/152-shadcn-line-default/styles.css create mode 100644 benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx create mode 100644 benchmarks/conformance/cases/153-shadcn-line-dots-colors/styles.css create mode 100644 benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx create mode 100644 benchmarks/conformance/cases/154-shadcn-line-dots-custom/styles.css create mode 100644 benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx create mode 100644 benchmarks/conformance/cases/155-shadcn-line-dots/styles.css create mode 100644 benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx create mode 100644 benchmarks/conformance/cases/156-shadcn-line-interactive/styles.css create mode 100644 benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx create mode 100644 benchmarks/conformance/cases/157-shadcn-line-label-custom/styles.css create mode 100644 benchmarks/conformance/cases/158-shadcn-line-label/example.tsx create mode 100644 benchmarks/conformance/cases/158-shadcn-line-label/styles.css create mode 100644 benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx create mode 100644 benchmarks/conformance/cases/159-shadcn-line-linear/styles.css create mode 100644 benchmarks/conformance/cases/16-lollipop/example.tsx create mode 100644 benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx create mode 100644 benchmarks/conformance/cases/160-shadcn-line-multiple/styles.css create mode 100644 benchmarks/conformance/cases/161-shadcn-line-step/example.tsx create mode 100644 benchmarks/conformance/cases/161-shadcn-line-step/styles.css create mode 100644 benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx create mode 100644 benchmarks/conformance/cases/162-shadcn-pie-donut-active/styles.css create mode 100644 benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx create mode 100644 benchmarks/conformance/cases/163-shadcn-pie-donut/styles.css create mode 100644 benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx create mode 100644 benchmarks/conformance/cases/164-shadcn-pie-interactive/styles.css create mode 100644 benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx create mode 100644 benchmarks/conformance/cases/165-shadcn-pie-label-custom/styles.css create mode 100644 benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx create mode 100644 benchmarks/conformance/cases/166-shadcn-pie-label-list/styles.css create mode 100644 benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx create mode 100644 benchmarks/conformance/cases/167-shadcn-pie-label/styles.css create mode 100644 benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx create mode 100644 benchmarks/conformance/cases/168-shadcn-pie-legend/styles.css create mode 100644 benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx create mode 100644 benchmarks/conformance/cases/169-shadcn-pie-separator-none/styles.css create mode 100644 benchmarks/conformance/cases/17-dumbbell/example.tsx create mode 100644 benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx create mode 100644 benchmarks/conformance/cases/170-shadcn-pie-simple/styles.css create mode 100644 benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx create mode 100644 benchmarks/conformance/cases/171-shadcn-pie-stacked/styles.css create mode 100644 benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx create mode 100644 benchmarks/conformance/cases/172-shadcn-radar-default/styles.css create mode 100644 benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx create mode 100644 benchmarks/conformance/cases/173-shadcn-radar-dots/styles.css create mode 100644 benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx create mode 100644 benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/styles.css create mode 100644 benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx create mode 100644 benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/styles.css create mode 100644 benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx create mode 100644 benchmarks/conformance/cases/176-shadcn-radar-grid-circle/styles.css create mode 100644 benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx create mode 100644 benchmarks/conformance/cases/177-shadcn-radar-grid-custom/styles.css create mode 100644 benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx create mode 100644 benchmarks/conformance/cases/178-shadcn-radar-grid-fill/styles.css create mode 100644 benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx create mode 100644 benchmarks/conformance/cases/179-shadcn-radar-grid-none/styles.css create mode 100644 benchmarks/conformance/cases/18-cumulative-histogram/example.tsx create mode 100644 benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx create mode 100644 benchmarks/conformance/cases/180-shadcn-radar-icons/styles.css create mode 100644 benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx create mode 100644 benchmarks/conformance/cases/181-shadcn-radar-label-custom/styles.css create mode 100644 benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx create mode 100644 benchmarks/conformance/cases/182-shadcn-radar-legend/styles.css create mode 100644 benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx create mode 100644 benchmarks/conformance/cases/183-shadcn-radar-lines-only/styles.css create mode 100644 benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx create mode 100644 benchmarks/conformance/cases/184-shadcn-radar-radius/styles.css create mode 100644 benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx create mode 100644 benchmarks/conformance/cases/185-shadcn-radial-grid/styles.css create mode 100644 benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx create mode 100644 benchmarks/conformance/cases/186-shadcn-radial-label/styles.css create mode 100644 benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx create mode 100644 benchmarks/conformance/cases/187-shadcn-radial-shape/styles.css create mode 100644 benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx create mode 100644 benchmarks/conformance/cases/188-shadcn-radial-simple/styles.css create mode 100644 benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx create mode 100644 benchmarks/conformance/cases/189-shadcn-radial-stacked/styles.css create mode 100644 benchmarks/conformance/cases/19-moving-average-line/example.tsx create mode 100644 benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx create mode 100644 benchmarks/conformance/cases/190-shadcn-tooltip-default/styles.css create mode 100644 benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx create mode 100644 benchmarks/conformance/cases/191-shadcn-tooltip-formatter/styles.css create mode 100644 benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx create mode 100644 benchmarks/conformance/cases/192-shadcn-tooltip-icons/styles.css create mode 100644 benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx create mode 100644 benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/styles.css create mode 100644 benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx create mode 100644 benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/styles.css create mode 100644 benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx create mode 100644 benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/styles.css create mode 100644 benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx create mode 100644 benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/styles.css create mode 100644 benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx create mode 100644 benchmarks/conformance/cases/197-shadcn-tooltip-label-none/styles.css create mode 100644 benchmarks/conformance/cases/20-normalized-stacked-area/example.tsx create mode 100644 benchmarks/conformance/cases/21-streamgraph/example.tsx create mode 100644 benchmarks/conformance/cases/22-bollinger-band/example.tsx create mode 100644 benchmarks/conformance/cases/24-quantitative-binned-heatmap/example.tsx create mode 100644 benchmarks/conformance/cases/25-calendar-heatmap/example.tsx create mode 100644 benchmarks/conformance/cases/26-diverging-likert/example.tsx create mode 100644 benchmarks/conformance/cases/27-parallel-coordinates/example.tsx create mode 100644 benchmarks/conformance/cases/28-candlestick/example.tsx create mode 100644 benchmarks/conformance/cases/29-waterfall/example.tsx create mode 100644 benchmarks/conformance/cases/30-slopegraph/example.tsx create mode 100644 benchmarks/conformance/cases/31-linear-regression/example.tsx create mode 100644 benchmarks/conformance/cases/32-change-arrows/example.tsx create mode 100644 benchmarks/conformance/cases/33-difference-chart/example.tsx create mode 100644 benchmarks/conformance/cases/34-pointer-tooltip/example.tsx create mode 100644 benchmarks/conformance/cases/35-grouped-tooltip/example.tsx create mode 100644 benchmarks/conformance/cases/36-hierarchy-tree/example.tsx create mode 100644 benchmarks/conformance/cases/37-delaunay-network/example.tsx create mode 100644 benchmarks/conformance/cases/38-contour-topography/example.tsx create mode 100644 benchmarks/conformance/cases/39-density-contours/example.tsx create mode 100644 benchmarks/conformance/cases/40-force-directed-network/example.tsx create mode 100644 benchmarks/conformance/cases/40-geojson-map/example.tsx create mode 100644 benchmarks/conformance/cases/41-waffle-unit-chart/example.tsx create mode 100644 benchmarks/conformance/cases/42-vector-field/example.tsx create mode 100644 benchmarks/conformance/cases/43-hexbin-density/example.tsx create mode 100644 benchmarks/conformance/cases/44-framed-scatter/example.tsx create mode 100644 benchmarks/conformance/cases/50-empirical-cdf/example.tsx create mode 100644 benchmarks/conformance/cases/51-faceted-distributions/example.tsx create mode 100644 benchmarks/conformance/cases/52-beeswarm-dodge/example.tsx create mode 100644 benchmarks/conformance/cases/53-log-scale-scatter/example.tsx create mode 100644 benchmarks/conformance/cases/54-bump-ranking/example.tsx create mode 100644 benchmarks/conformance/cases/55-indexed-multi-line/example.tsx create mode 100644 benchmarks/conformance/cases/56-connected-scatter/example.tsx create mode 100644 benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx create mode 100644 benchmarks/conformance/cases/58-select-extrema/example.tsx create mode 100644 benchmarks/conformance/cases/59-grouped-reducer-bars/example.tsx create mode 100644 benchmarks/conformance/cases/60-lag-autocorrelation/example.tsx create mode 100644 benchmarks/conformance/cases/61-quantile-ribbon/example.tsx create mode 100644 benchmarks/conformance/cases/62-ridgeline-density/example.tsx create mode 100644 benchmarks/conformance/cases/63-violin-distributions/example.tsx create mode 100644 benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx create mode 100644 benchmarks/conformance/cases/65-voronoi-nearest-tooltip/example.tsx create mode 100644 benchmarks/conformance/cases/70-composed-chart/example.tsx create mode 100644 benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx create mode 100644 benchmarks/conformance/cases/72-recharts-mixed-bars/example.tsx create mode 100644 benchmarks/conformance/cases/73-many-point-scatter/example.tsx create mode 100644 benchmarks/conformance/cases/74-recharts-treemap/example.tsx create mode 100644 benchmarks/conformance/cases/75-radar/example.tsx create mode 100644 benchmarks/conformance/cases/76-pie/example.tsx create mode 100644 benchmarks/conformance/cases/77-donut/example.tsx create mode 100644 benchmarks/conformance/cases/78-gauge/example.tsx create mode 100644 benchmarks/conformance/cases/80-echarts-axis-pointer/example.tsx create mode 100644 benchmarks/conformance/cases/81-recharts-interactive-legend/example.tsx create mode 100644 benchmarks/conformance/cases/82-chart-table-selection/example.tsx create mode 100644 benchmarks/conformance/cases/83-focus-context-window/example.tsx create mode 100644 benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/example.tsx create mode 100644 benchmarks/conformance/cases/85-scrollable-resource-lanes/example.tsx create mode 100644 benchmarks/conformance/cases/86-streaming-window-preservation/example.tsx create mode 100644 benchmarks/conformance/cases/87-echarts-synchronized-cursors/example.tsx create mode 100644 benchmarks/conformance/cases/88-echarts-free-cursor/example.tsx create mode 100644 benchmarks/conformance/cases/89-brush-range-selection/example.tsx create mode 100644 benchmarks/conformance/cases/90-zoomable-time-window/example.tsx create mode 100644 benchmarks/conformance/cases/91-timeline-playback-scrubber/example.tsx create mode 100644 benchmarks/conformance/cases/92-editable-event-range/example.tsx create mode 100644 benchmarks/conformance/cases/93-labeled-pie/example.tsx create mode 100644 benchmarks/conformance/cases/94-center-donut/example.tsx create mode 100644 benchmarks/conformance/cases/95-rounded-donut/example.tsx create mode 100644 benchmarks/conformance/cases/96-nested-donut/example.tsx create mode 100644 benchmarks/conformance/cases/97-rose/example.tsx create mode 100644 benchmarks/conformance/cases/98-needle-gauge/example.tsx create mode 100644 benchmarks/conformance/cases/99-comparative-radar/example.tsx create mode 100644 benchmarks/conformance/cases/bar-grouped/example.tsx create mode 100644 benchmarks/conformance/cases/bar-horizontal-ranking/example.tsx create mode 100644 benchmarks/conformance/cases/bar-stacked/example.tsx create mode 100644 benchmarks/conformance/cases/bar-vertical-sorted/example.tsx create mode 100644 benchmarks/conformance/cases/facets-anscombe/example.tsx create mode 100644 benchmarks/conformance/cases/heatmap-labeled/example.tsx create mode 100644 benchmarks/conformance/cases/histogram/example.tsx create mode 100644 benchmarks/conformance/cases/scatter-bubble/example.tsx create mode 100644 packages/charts-demo-data/src/country-atlas.ts create mode 100644 packages/charts-demo-data/src/learning-poverty-geography.ts rename benchmarks/conformance/shadcn/area-interactive-data.json => packages/charts-demo-data/src/shadcn-area-interactive-data.json (100%) create mode 100644 packages/charts-demo-data/src/shadcn.ts create mode 100644 packages/charts-demo-data/src/simplify-geo.ts create mode 100644 scripts/check-catalog-examples.mjs create mode 100644 scripts/clean-catalog-examples.mjs create mode 100644 scripts/shadcn-example-source.mjs create mode 100644 scripts/typescript-source.mjs diff --git a/API-FRICTION.md b/API-FRICTION.md index 9b4b35a6..1ac98088 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -854,6 +854,11 @@ Each entry records: two broad `import.meta.glob('./cases/*/*.ts')` registries. Its default entry registered 584 implementation and raw-source imports, including every comparison renderer, data/helper modules, and a `tanstack.test.ts` file. + Even after the production graph was narrowed, its public source entry was + still the conformance `tanstack.ts` adapter. Opening an example in the docs + therefore started on mount plumbing and followed shared harness modules + instead of showing the chart definition; some sandboxes contained source + from unrelated examples. - Decision: give each chart type an isolated entry module and share only the renderer-free host setup. Renderer-specific helpers with runtime imports live in separate modules. Tier variants also use direct build-time globals; @@ -883,7 +888,16 @@ Each entry records: Every published TanStack root receives the same static-closure comparison-package check. The schema-v3 artifact validator rejects unreferenced files, unsafe paths, missing imports, invalid preloads, invalid - authored-source roles, and a comparison module not marked debug-only. + authored-source roles, and a comparison module not marked debug-only. The + source catalog now publishes one case-local `example.tsx` root per case. + Conformance adapters import that public definition, while the docs runtime + supplies its own hidden React mount entry and rejects any relative import + that leaves the case directory. `pnpm catalog:examples:check` validates all + 188 public roots, their default exports, definition-first source order, + adapter ownership, unused source, and isolated relative source closures. The + largest public TypeScript closure contains four files. The shadcn generator + prunes unused variant helpers before writing each public example, while + hidden catalog adapters retain deterministic preview framing and focus. ### F-025 — Bundle maintenance clobbered the full comparison report diff --git a/benchmarks/conformance/DEFINITION-COVERAGE-AUDIT.md b/benchmarks/conformance/DEFINITION-COVERAGE-AUDIT.md index 5308ba7d..66bca66c 100644 --- a/benchmarks/conformance/DEFINITION-COVERAGE-AUDIT.md +++ b/benchmarks/conformance/DEFINITION-COVERAGE-AUDIT.md @@ -119,74 +119,74 @@ ownership boundary. | Case | Disposition | Current definition boundary | | -------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| [01 — Line gaps](./cases/01-line-gaps/tanstack.ts) | Definition now | A nullable native line channel owns discontinuity geometry and source focus lineage. | -| [03 — Temperature range](./cases/03-temperature-range-band/tanstack.ts) | Definition now | Native area and line marks consume the same observations without range DTOs. | -| [04 — Stacked time area](./cases/04-stacked-time-area/tanstack.ts) | Definition now | Native stacking, scales, guides, and legend consume tidy industry rows. | -| [13 — Interval timeline](./cases/13-interval-timeline/tanstack.ts) | Definition now | Native horizontal interval bars own open-to-close endpoints and band geometry. | -| [16 — Lollipop](./cases/16-lollipop/tanstack.ts) | Definition now | Native links and dots express the pattern without a lollipop-specific primitive. | -| [17 — Dumbbell](./cases/17-dumbbell/tanstack.ts) | Definition now | One native endpoint link and two dot layers remain source-bound. | -| [18 — Cumulative histogram](./cases/18-cumulative-histogram/tanstack.ts) | Definition now | Public `binX` and `cumulative` outputs feed native rectangles. | -| [19 — Moving average](./cases/19-moving-average-line/tanstack.ts) | Definition now | The public `window` transform owns rolling means before native lines. | -| [20 — Normalized stacked area](./cases/20-normalized-stacked-area/tanstack.ts) | Definition now | Public normalized stack layout remains inside the area definition. | -| [24 — Quantitative binned heatmap](./cases/24-quantitative-binned-heatmap/tanstack.ts) | Definition now | Public `binXY` emits semantic intervals consumed by native rectangles. | -| [25 — Calendar heatmap](./cases/25-calendar-heatmap/tanstack.ts) | Definition now | Calendar accessors are authored semantics; native cells and band scales own geometry. | -| [28 — Candlestick](./cases/28-candlestick/tanstack.ts) | Definition now | Native links express high-low and open-close intervals; gain/loss is presentation policy. | -| [32 — Change arrows](./cases/32-change-arrows/tanstack.ts) | Definition now | Native arrow endpoints remove any need for prepared paths. | -| [34 — Pointer tooltip](./cases/34-pointer-tooltip/tanstack.ts) | Definition now | Native focus, decorative line ownership, and point tooltip behavior live in the definition. | -| [35 — Grouped tooltip](./cases/35-grouped-tooltip/tanstack.ts) | Definition now | Native grouped focus and ordered portal tooltip behavior live in the definition. | -| [40 — GeoJSON map](./cases/40-geojson-map/tanstack.ts) | Definition now | `geoShape` and a public projection descriptor own fitting and path rendering. | -| [42 — Vector field](./cases/42-vector-field/tanstack.ts) | Definition now | The native vector mark owns geometry and one interaction point per sampled source row; focused sampling and runtime tests prove the boundary. | -| [44 — Framed scatter](./cases/44-framed-scatter/tanstack.ts) | Definition now | Native frame and dot marks share ordinary Cartesian scales. | -| [50 — Empirical CDF](./cases/50-empirical-cdf/tanstack.ts) | Definition now | Public tied ranking plus explicit probability mapping feeds a native stepped line. | -| [53 — Log scatter](./cases/53-log-scale-scatter/tanstack.ts) | Definition now | A source-domain guard and configured log scale feed native dots directly. | -| [54 — Bump ranking](./cases/54-bump-ranking/tanstack.ts) | Definition now | Public ranking and maximum-date selection feed native line, dot, and text marks. | -| [59 — Grouped reducer bars](./cases/59-grouped-reducer-bars/tanstack.ts) | Definition now | Public grouped mean output feeds native bars and labels. | -| [60 — Lag autocorrelation](./cases/60-lag-autocorrelation/tanstack.ts) | Definition now | Public two-row windows form lag pairs for native dots and a reference line. | -| [70 — Composed chart](./cases/70-composed-chart/tanstack.ts) | First-party primitive | Native `barY.maxThickness` applies a centered cap after final band, subgroup, and inset resolution; one private resolver also serves `barX`. | -| [72 — Mixed bars](./cases/72-recharts-mixed-bars/tanstack.ts) | Definition now | Public grouped bar layout expresses adjacent stacked and independent slots. | -| [73 — Many-point scatter](./cases/73-many-point-scatter/tanstack.ts) | Definition now | Native dots own explicit keys and a configured radius scale over selected source rows. | -| [102 — World choropleth](./cases/102-world-choropleth/tanstack.ts) | Definition now | `geoShape`, a public Equal Earth descriptor, and threshold color render joined countries. | -| [103 — Bubble map](./cases/103-bubble-map/tanstack.ts) | Definition now | `geoShape` owns projected point geometry while an ordinary radius channel carries population. | -| [104 — Orthographic globe](./cases/104-orthographic-globe/tanstack.ts) | Definition now | Sphere, graticule, and land are ordinary `geoShape` layers with a public projection descriptor. | -| [105 — Route map](./cases/105-route-map/tanstack.ts) | Definition now | Land, graticule, route, and sphere share one public Equal Earth descriptor. | -| [106 — Polar line](./cases/106-polar-line/tanstack.ts) | Definition now | Native polar grids and radial line consume semantic angle and radius channels. | -| [107 — Polar scatter](./cases/107-polar-scatter/tanstack.ts) | Definition now | Native polar grids and radial dots consume wind direction, band, and speed channels. | -| [108 — Country choropleth](./cases/108-country-choropleth/tanstack.ts) | Definition now | `geoShape` plus a public projection descriptor and quantized color render joined countries. | -| [109 — US choropleth](./cases/109-us-state-choropleth/tanstack.ts) | Definition now | County features pass the public Albers USA descriptor to `geoShape`; focused tests prove descriptor ownership, fitting, lineage, and quantile color. | -| [Grouped bars](./cases/bar-grouped/tanstack.ts) | Definition now | Public grouped reduction and bar layout stay inside the responsive definition. | -| [Horizontal ranking bars](./cases/bar-horizontal-ranking/tanstack.ts) | Definition now | Authored ordering feeds native horizontal bars and automatic label measurement. | -| [Stacked bars](./cases/bar-stacked/tanstack.ts) | Definition now | Public `fold` and `stack` outputs feed native bars directly. | -| [Sorted vertical bars](./cases/bar-vertical-sorted/tanstack.ts) | Definition now | Native bars own geometry; responsive input changes only tick-label rotation. | -| [Anscombe facets](./cases/facets-anscombe/tanstack.ts) | Definition now | Native facet composition owns child bounds and source-bound dot scenes. | -| [Labeled heatmap](./cases/heatmap-labeled/tanstack.ts) | Definition now | Native cells and text share explicit category and color scales. | -| [Histogram](./cases/histogram/tanstack.ts) | Definition now | Public `binX` output feeds native semantic rectangle intervals. | -| [Bubble scatter](./cases/scatter-bubble/tanstack.ts) | Definition now | Native dot color and radius channels consume model rows directly. | +| [01 — Line gaps](./cases/01-line-gaps/example.tsx) | Definition now | A nullable native line channel owns discontinuity geometry and source focus lineage. | +| [03 — Temperature range](./cases/03-temperature-range-band/example.tsx) | Definition now | Native area and line marks consume the same observations without range DTOs. | +| [04 — Stacked time area](./cases/04-stacked-time-area/example.tsx) | Definition now | Native stacking, scales, guides, and legend consume tidy industry rows. | +| [13 — Interval timeline](./cases/13-interval-timeline/example.tsx) | Definition now | Native horizontal interval bars own open-to-close endpoints and band geometry. | +| [16 — Lollipop](./cases/16-lollipop/example.tsx) | Definition now | Native links and dots express the pattern without a lollipop-specific primitive. | +| [17 — Dumbbell](./cases/17-dumbbell/example.tsx) | Definition now | One native endpoint link and two dot layers remain source-bound. | +| [18 — Cumulative histogram](./cases/18-cumulative-histogram/example.tsx) | Definition now | Public `binX` and `cumulative` outputs feed native rectangles. | +| [19 — Moving average](./cases/19-moving-average-line/example.tsx) | Definition now | The public `window` transform owns rolling means before native lines. | +| [20 — Normalized stacked area](./cases/20-normalized-stacked-area/example.tsx) | Definition now | Public normalized stack layout remains inside the area definition. | +| [24 — Quantitative binned heatmap](./cases/24-quantitative-binned-heatmap/example.tsx) | Definition now | Public `binXY` emits semantic intervals consumed by native rectangles. | +| [25 — Calendar heatmap](./cases/25-calendar-heatmap/example.tsx) | Definition now | Calendar accessors are authored semantics; native cells and band scales own geometry. | +| [28 — Candlestick](./cases/28-candlestick/example.tsx) | Definition now | Native links express high-low and open-close intervals; gain/loss is presentation policy. | +| [32 — Change arrows](./cases/32-change-arrows/example.tsx) | Definition now | Native arrow endpoints remove any need for prepared paths. | +| [34 — Pointer tooltip](./cases/34-pointer-tooltip/example.tsx) | Definition now | Native focus, decorative line ownership, and point tooltip behavior live in the definition. | +| [35 — Grouped tooltip](./cases/35-grouped-tooltip/example.tsx) | Definition now | Native grouped focus and ordered portal tooltip behavior live in the definition. | +| [40 — GeoJSON map](./cases/40-geojson-map/example.tsx) | Definition now | `geoShape` and a public projection descriptor own fitting and path rendering. | +| [42 — Vector field](./cases/42-vector-field/example.tsx) | Definition now | The native vector mark owns geometry and one interaction point per sampled source row; focused sampling and runtime tests prove the boundary. | +| [44 — Framed scatter](./cases/44-framed-scatter/example.tsx) | Definition now | Native frame and dot marks share ordinary Cartesian scales. | +| [50 — Empirical CDF](./cases/50-empirical-cdf/example.tsx) | Definition now | Public tied ranking plus explicit probability mapping feeds a native stepped line. | +| [53 — Log scatter](./cases/53-log-scale-scatter/example.tsx) | Definition now | A source-domain guard and configured log scale feed native dots directly. | +| [54 — Bump ranking](./cases/54-bump-ranking/example.tsx) | Definition now | Public ranking and maximum-date selection feed native line, dot, and text marks. | +| [59 — Grouped reducer bars](./cases/59-grouped-reducer-bars/example.tsx) | Definition now | Public grouped mean output feeds native bars and labels. | +| [60 — Lag autocorrelation](./cases/60-lag-autocorrelation/example.tsx) | Definition now | Public two-row windows form lag pairs for native dots and a reference line. | +| [70 — Composed chart](./cases/70-composed-chart/example.tsx) | First-party primitive | Native `barY.maxThickness` applies a centered cap after final band, subgroup, and inset resolution; one private resolver also serves `barX`. | +| [72 — Mixed bars](./cases/72-recharts-mixed-bars/example.tsx) | Definition now | Public grouped bar layout expresses adjacent stacked and independent slots. | +| [73 — Many-point scatter](./cases/73-many-point-scatter/example.tsx) | Definition now | Native dots own explicit keys and a configured radius scale over selected source rows. | +| [102 — World choropleth](./cases/102-world-choropleth/example.tsx) | Definition now | `geoShape`, a public Equal Earth descriptor, and threshold color render joined countries. | +| [103 — Bubble map](./cases/103-bubble-map/example.tsx) | Definition now | `geoShape` owns projected point geometry while an ordinary radius channel carries population. | +| [104 — Orthographic globe](./cases/104-orthographic-globe/example.tsx) | Definition now | Sphere, graticule, and land are ordinary `geoShape` layers with a public projection descriptor. | +| [105 — Route map](./cases/105-route-map/example.tsx) | Definition now | Land, graticule, route, and sphere share one public Equal Earth descriptor. | +| [106 — Polar line](./cases/106-polar-line/example.tsx) | Definition now | Native polar grids and radial line consume semantic angle and radius channels. | +| [107 — Polar scatter](./cases/107-polar-scatter/example.tsx) | Definition now | Native polar grids and radial dots consume wind direction, band, and speed channels. | +| [108 — Country choropleth](./cases/108-country-choropleth/example.tsx) | Definition now | `geoShape` plus a public projection descriptor and quantized color render joined countries. | +| [109 — US choropleth](./cases/109-us-state-choropleth/example.tsx) | Definition now | County features pass the public Albers USA descriptor to `geoShape`; focused tests prove descriptor ownership, fitting, lineage, and quantile color. | +| [Grouped bars](./cases/bar-grouped/example.tsx) | Definition now | Public grouped reduction and bar layout stay inside the responsive definition. | +| [Horizontal ranking bars](./cases/bar-horizontal-ranking/example.tsx) | Definition now | Authored ordering feeds native horizontal bars and automatic label measurement. | +| [Stacked bars](./cases/bar-stacked/example.tsx) | Definition now | Public `fold` and `stack` outputs feed native bars directly. | +| [Sorted vertical bars](./cases/bar-vertical-sorted/example.tsx) | Definition now | Native bars own geometry; responsive input changes only tick-label rotation. | +| [Anscombe facets](./cases/facets-anscombe/example.tsx) | Definition now | Native facet composition owns child bounds and source-bound dot scenes. | +| [Labeled heatmap](./cases/heatmap-labeled/example.tsx) | Definition now | Native cells and text share explicit category and color scales. | +| [Histogram](./cases/histogram/example.tsx) | Definition now | Public `binX` output feeds native semantic rectangle intervals. | +| [Bubble scatter](./cases/scatter-bubble/example.tsx) | Definition now | Native dot color and radius channels consume model rows directly. | ## Statistical and prepared-data cases | Case | Disposition | Target definition boundary | | ------------------------------------------------------------------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [02 — End labels](./cases/02-multi-line-end-labels/tanstack.ts) | Definition now | Use grouped `select` by maximum date directly as the `text` data, preserving the intended endpoint under row reordering. | -| [14 — Error bars](./cases/14-error-bars/tanstack.ts) | Definition now | Use current `groupBy` mean and deviation outputs with native rule and tick marks; the estimator choice remains authored. | -| [15 — Boxplot](./cases/15-boxplot/tanstack.ts) | First-party primitive | Add `boxX` / `boxY`; quartiles, fences, whiskers, and outlier partitioning are canonical composite-mark work. | -| [21 — Streamgraph](./cases/21-streamgraph/tanstack.ts) | First-party primitive | Add `inside-out` stack order to the current `stack({ offset: 'wiggle' })`; native `areaY` already owns the geometry. | -| [22 — Bollinger band](./cases/22-bollinger-band/tanstack.ts) | Definition now | Call current `window` mean and deviation outputs beside one `areaY` and one `lineY`. | -| [26 — Diverging Likert](./cases/26-diverging-likert/tanstack.ts) | First-party primitive | Add an anchored stack offset that can center a named or fractional neutral series; current `groupBy` already owns the counts. | +| [02 — End labels](./cases/02-multi-line-end-labels/example.tsx) | Definition now | Use grouped `select` by maximum date directly as the `text` data, preserving the intended endpoint under row reordering. | +| [14 — Error bars](./cases/14-error-bars/example.tsx) | Definition now | Use current `groupBy` mean and deviation outputs with native rule and tick marks; the estimator choice remains authored. | +| [15 — Boxplot](./cases/15-boxplot/example.tsx) | First-party primitive | Add `boxX` / `boxY`; quartiles, fences, whiskers, and outlier partitioning are canonical composite-mark work. | +| [21 — Streamgraph](./cases/21-streamgraph/example.tsx) | First-party primitive | Add `inside-out` stack order to the current `stack({ offset: 'wiggle' })`; native `areaY` already owns the geometry. | +| [22 — Bollinger band](./cases/22-bollinger-band/example.tsx) | Definition now | Call current `window` mean and deviation outputs beside one `areaY` and one `lineY`. | +| [26 — Diverging Likert](./cases/26-diverging-likert/example.tsx) | First-party primitive | Add an anchored stack offset that can center a named or fractional neutral series; current `groupBy` already owns the counts. | | [27 — Parallel coordinates](./cases/27-parallel-coordinates/transform.ts) | First-party primitive | Add `fold`, followed by current grouped `normalize`; metric direction remains a domain accessor. | -| [29 — Waterfall](./cases/29-waterfall/tanstack.ts) | First-party primitive | Add an eager waterfall layout that returns stable `start`, `end`, `delta`, and total rows. | +| [29 — Waterfall](./cases/29-waterfall/example.tsx) | First-party primitive | Add an eager waterfall layout that returns stable `start`, `end`, `delta`, and total rows. | | [30 — Slopegraph](./cases/30-slopegraph/transform.ts) | First-party primitive | Use `fold` for the two period fields; current line, text, and endpoint selection marks suffice. | -| [31 — Regression](./cases/31-linear-regression/tanstack.ts) | First-party primitive | Add `linearRegressionX` / `linearRegressionY`, optionally including a confidence band. | -| [33 — Difference](./cases/33-difference-chart/tanstack.ts) | First-party primitive | Add `differenceX` / `differenceY`; crossing interpolation and positive/negative clipping belong to the composite mark. | -| [51 — Faceted distributions](./cases/51-faceted-distributions/tanstack.ts) | Definition now | Compose current grouped `binX`, facet-local `normalize`, and `facet` in the definition. | -| [55 — Indexed lines](./cases/55-indexed-multi-line/tanstack.ts) | Definition now | Use grouped `normalize({ basis: 'first' })` and grouped maximum-date `select`. | -| [56 — Connected scatter](./cases/56-connected-scatter/tanstack.ts) | Definition now | Use current two-row `window` output with the native `arrow`; the sparse arrow filter is editorial intent. | -| [57 — Marginal histograms](./cases/57-scatter-marginal-histograms/tanstack.ts) | First-party primitive | Add a multi-view composition with a main view plus top and right views, shared primary scales, and independent count scales. | -| [58 — Extrema](./cases/58-select-extrema/tanstack.ts) | Definition now | Feed current min/max `select` results directly to native dot and text marks. | -| [61 — Quantile ribbon](./cases/61-quantile-ribbon/tanstack.ts) | Definition now | Feed current grouped quantile reducers directly to `areaY` and `lineY`. | +| [31 — Regression](./cases/31-linear-regression/example.tsx) | First-party primitive | Add `linearRegressionX` / `linearRegressionY`, optionally including a confidence band. | +| [33 — Difference](./cases/33-difference-chart/example.tsx) | First-party primitive | Add `differenceX` / `differenceY`; crossing interpolation and positive/negative clipping belong to the composite mark. | +| [51 — Faceted distributions](./cases/51-faceted-distributions/example.tsx) | Definition now | Compose current grouped `binX`, facet-local `normalize`, and `facet` in the definition. | +| [55 — Indexed lines](./cases/55-indexed-multi-line/example.tsx) | Definition now | Use grouped `normalize({ basis: 'first' })` and grouped maximum-date `select`. | +| [56 — Connected scatter](./cases/56-connected-scatter/example.tsx) | Definition now | Use current two-row `window` output with the native `arrow`; the sparse arrow filter is editorial intent. | +| [57 — Marginal histograms](./cases/57-scatter-marginal-histograms/example.tsx) | First-party primitive | Add a multi-view composition with a main view plus top and right views, shared primary scales, and independent count scales. | +| [58 — Extrema](./cases/58-select-extrema/example.tsx) | Definition now | Feed current min/max `select` results directly to native dot and text marks. | +| [61 — Quantile ribbon](./cases/61-quantile-ribbon/example.tsx) | Definition now | Feed current grouped quantile reducers directly to `areaY` and `lineY`. | | [62 — Ridgeline](./cases/62-ridgeline-density/transform.ts) | First-party primitive | Add a ridge layout over binned or density rows so categorical baselines, overlap, domains, and tick labels stay semantic. | | [63 — Violin](./cases/63-violin-distributions/transform.ts) | First-party primitive | Keep grouped bins, max normalization, and medians explicit; add `violinX` / `violinY` only for mirrored category-step geometry and let ordinary ticks render summaries. | -| [64 — Marimekko](./cases/64-marimekko-mosaic/tanstack.ts) | First-party primitive | Keep grouped count or sum policy explicit, then use `mosaicY` / `mosaicX` for the two normalized interval dimensions; pie shares the proportional allocator, while waffle keeps unit fragmentation and pixel packing. | -| [71 — Population pyramid](./cases/71-recharts-population-pyramid/tanstack.ts) | Definition now | Public `groupBy`, one signed value accessor, and one native diverging `barX` stack express the chart with complete observation lineage; no new primitive is warranted. | +| [64 — Marimekko](./cases/64-marimekko-mosaic/example.tsx) | First-party primitive | Keep grouped count or sum policy explicit, then use `mosaicY` / `mosaicX` for the two normalized interval dimensions; pie shares the proportional allocator, while waffle keeps unit fragmentation and pixel packing. | +| [71 — Population pyramid](./cases/71-recharts-population-pyramid/example.tsx) | Definition now | Public `groupBy`, one signed value accessor, and one native diverging `barX` stack express the chart with complete observation lineage; no new primitive is warranted. | | [75 — Radar](./cases/75-radar/transform.ts) | First-party primitive | Use generic `fold` plus current extent normalization and `radialArea`. `angleGrid` already exposes label context and correct anchor/baseline defaults; the same-reference 1.1px `labelDy` and compact margin remain explicit presentation, not a shared helper. | | [99 — Comparative radar](./cases/99-comparative-radar/transform.ts) | First-party primitive | Use the same `fold` and grouped normalization while retaining profile identity. It keeps the same explicit 1.1px angle-label offset and compact margin for reference parity without adding a radar helper. | @@ -194,134 +194,134 @@ ownership boundary. | Case | Disposition | Target definition boundary | | ---------------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [36 — Hierarchy tree](./cases/36-hierarchy-tree/tanstack.ts) | Optional primitive | Add a granular `tree` mark that accepts path or parent identity and owns stratification, node/link layout, and source lineage. | -| [37 — Delaunay network](./cases/37-delaunay-network/tanstack.ts) | Optional primitive | Add `delaunayLink` in a spatial subpath; triangulation must use final screen positions. | -| [38 — Contour topography](./cases/38-contour-topography/tanstack.ts) | Optional primitive | A current `geoShape` can replace the raw renderer immediately, but a granular `contour` mark should own grid-to-isoline geometry. | -| [39 — Density contours](./cases/39-density-contours/tanstack.ts) | Optional primitive | Add `densityContour`; the estimator needs final scales and inner bounds and should emit normal interaction lineage. | +| [36 — Hierarchy tree](./cases/36-hierarchy-tree/example.tsx) | Optional primitive | Add a granular `tree` mark that accepts path or parent identity and owns stratification, node/link layout, and source lineage. | +| [37 — Delaunay network](./cases/37-delaunay-network/example.tsx) | Optional primitive | Add `delaunayLink` in a spatial subpath; triangulation must use final screen positions. | +| [38 — Contour topography](./cases/38-contour-topography/example.tsx) | Optional primitive | A current `geoShape` can replace the raw renderer immediately, but a granular `contour` mark should own grid-to-isoline geometry. | +| [39 — Density contours](./cases/39-density-contours/example.tsx) | Optional primitive | Add `densityContour`; the estimator needs final scales and inner bounds and should emit normal interaction lineage. | | [40 — Force network](./cases/40-force-directed-network/layout.ts) | Optional primitive | Add a granular force-layout mark or transform with explicit forces, determinism, and static/live policy; modeling choices become definition options. | -| [41 — Waffle](./cases/41-waffle-unit-chart/tanstack.ts) | First-party primitive | Add `waffleX` / `waffleY`; unit allocation and responsive packing are standard mark semantics. | -| [43 — Hexbin](./cases/43-hexbin-density/tanstack.ts) | Optional primitive | The resolved-screen `hexbin` primitive bins final projected pixels, internally inverts bin centers to semantic values for its adopted hexagon child, then remaps through the configured final scales; case code owns none of that round trip. | -| [52 — Beeswarm](./cases/52-beeswarm-dodge/tanstack.ts) | First-party primitive | Add `dodgeX` / `dodgeY` as dot layouts; preserve the measured coordinate exactly and resolve collisions after scaling. | -| [65 — Voronoi](./cases/65-voronoi-nearest-tooltip/tanstack.ts) | Optional primitive | Add visible `voronoi` cells as a spatial mark; nearest-point focus should continue through the native spatial index. | -| [74 — Treemap](./cases/74-recharts-treemap/tanstack.ts) | Optional primitive | Add a hierarchy subpath `treemap` that accepts raw hierarchy/value channels and owns coordinate convention and lineage. | -| [76 — Pie](./cases/76-pie/tanstack.ts) | First-party primitive | Add polar `pie()` allocation from a value channel, returning angle intervals, midpoint, fraction, key, and source datum. | -| [77 — Donut](./cases/77-donut/tanstack.ts) | First-party primitive | Use the same `pie()` layout with the current `radialArc` inner radius. | -| [78 — Gauge](./cases/78-gauge/tanstack.ts) | First-party primitive | Use partial-range `pie({ startAngle, endAngle })`; semantic agreement aggregation remains ordinary preparation. | -| [93 — Labeled pie](./cases/93-labeled-pie/tanstack.ts) | First-party primitive | Add pie allocation plus radial arc labels with pixel offset, automatic outside anchor, and optional leaders. | -| [94 — Center donut](./cases/94-center-donut/tanstack.ts) | First-party primitive | Use pie allocation and current center `radialText`; the displayed total is legitimate semantic reduction. | -| [95 — Rounded donut](./cases/95-rounded-donut/tanstack.ts) | First-party primitive | Give pie allocation explicit angular-gap semantics and keep current arc corner radius. | +| [41 — Waffle](./cases/41-waffle-unit-chart/example.tsx) | First-party primitive | Add `waffleX` / `waffleY`; unit allocation and responsive packing are standard mark semantics. | +| [43 — Hexbin](./cases/43-hexbin-density/example.tsx) | Optional primitive | The resolved-screen `hexbin` primitive bins final projected pixels, internally inverts bin centers to semantic values for its adopted hexagon child, then remaps through the configured final scales; case code owns none of that round trip. | +| [52 — Beeswarm](./cases/52-beeswarm-dodge/example.tsx) | First-party primitive | Add `dodgeX` / `dodgeY` as dot layouts; preserve the measured coordinate exactly and resolve collisions after scaling. | +| [65 — Voronoi](./cases/65-voronoi-nearest-tooltip/example.tsx) | Optional primitive | Add visible `voronoi` cells as a spatial mark; nearest-point focus should continue through the native spatial index. | +| [74 — Treemap](./cases/74-recharts-treemap/example.tsx) | Optional primitive | Add a hierarchy subpath `treemap` that accepts raw hierarchy/value channels and owns coordinate convention and lineage. | +| [76 — Pie](./cases/76-pie/example.tsx) | First-party primitive | Add polar `pie()` allocation from a value channel, returning angle intervals, midpoint, fraction, key, and source datum. | +| [77 — Donut](./cases/77-donut/example.tsx) | First-party primitive | Use the same `pie()` layout with the current `radialArc` inner radius. | +| [78 — Gauge](./cases/78-gauge/example.tsx) | First-party primitive | Use partial-range `pie({ startAngle, endAngle })`; semantic agreement aggregation remains ordinary preparation. | +| [93 — Labeled pie](./cases/93-labeled-pie/example.tsx) | First-party primitive | Add pie allocation plus radial arc labels with pixel offset, automatic outside anchor, and optional leaders. | +| [94 — Center donut](./cases/94-center-donut/example.tsx) | First-party primitive | Use pie allocation and current center `radialText`; the displayed total is legitimate semantic reduction. | +| [95 — Rounded donut](./cases/95-rounded-donut/example.tsx) | First-party primitive | Give pie allocation explicit angular-gap semantics and keep current arc corner radius. | | [96 — Nested donut](./cases/96-nested-donut/transform.ts) | First-party primitive | Run independent declarative pie layouts for each ring; domain-specific family aggregation remains prepared data. | -| [97 — Rose](./cases/97-rose/tanstack.ts) | First-party primitive | Add a radial bar whose categorical angle band and quantitative radius vary per datum. | -| [98 — Needle gauge](./cases/98-needle-gauge/tanstack.ts) | First-party primitive | Use pie allocation for threshold bands; current radial rule, dot, text, and guide marks already express the needle and ticks. | -| [100 — Radial bars](./cases/100-radial-bars/tanstack.ts) | First-party primitive | Add the transposed radial bar: categorical radius band with quantitative angle extent. | -| [101 — Sunburst](./cases/101-sunburst/tanstack.ts) | Optional primitive | Add a hierarchy subpath `sunburst` that owns partition layout and depth-dependent radii while exposing visual channels. | -| [110 — Projection gallery](./cases/110-projection-gallery/tanstack.ts) | Definition now | Use current `facet` with one child `geoShape` chart per projection; remove manual pane bounds and projection fitting. | -| [111 — Basic Sankey](./cases/111-basic-sankey/tanstack.ts) | Optional primitive | Add granular `sankeyDiagram({ nodes, links })`; hide mutation cloning, endpoint resolution, responsive layout, and DTO conversion. | -| [111 — Sankey flow](./cases/111-sankey-flow/tanstack.ts) | Optional primitive | Use the same Sankey primitive with authored node, link, label, and backdrop presentation channels. | -| [116 — Geometry morph](./cases/116-geometry-morph/tanstack.ts) | Inline custom mark | Keep the normalized-topology `createMark`; cross-type path-token continuity is the example's unique geometry contract. | +| [97 — Rose](./cases/97-rose/example.tsx) | First-party primitive | Add a radial bar whose categorical angle band and quantitative radius vary per datum. | +| [98 — Needle gauge](./cases/98-needle-gauge/example.tsx) | First-party primitive | Use pie allocation for threshold bands; current radial rule, dot, text, and guide marks already express the needle and ticks. | +| [100 — Radial bars](./cases/100-radial-bars/example.tsx) | First-party primitive | Add the transposed radial bar: categorical radius band with quantitative angle extent. | +| [101 — Sunburst](./cases/101-sunburst/example.tsx) | Optional primitive | Add a hierarchy subpath `sunburst` that owns partition layout and depth-dependent radii while exposing visual channels. | +| [110 — Projection gallery](./cases/110-projection-gallery/example.tsx) | Definition now | Use current `facet` with one child `geoShape` chart per projection; remove manual pane bounds and projection fitting. | +| [111 — Basic Sankey](./cases/111-basic-sankey/example.tsx) | Optional primitive | Add granular `sankeyDiagram({ nodes, links })`; hide mutation cloning, endpoint resolution, responsive layout, and DTO conversion. | +| [111 — Sankey flow](./cases/111-sankey-flow/example.tsx) | Optional primitive | Use the same Sankey primitive with authored node, link, label, and backdrop presentation channels. | +| [116 — Geometry morph](./cases/116-geometry-morph/example.tsx) | Inline custom mark | Keep the normalized-topology `createMark`; cross-type path-token continuity is the example's unique geometry contract. | ## Interaction and shell cases | Case | Disposition | Target definition or application boundary | | ---------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [80 — Axis pointer](./cases/80-echarts-axis-pointer/tanstack.ts) | Definition now | Resolved with `focusGuideX`, grouped x focus, native grouped tooltip, and the native color legend; only conformance observation remains outside. | -| [81 — Interactive legend](./cases/81-recharts-interactive-legend/tanstack.ts) | First-party primitive | Resolved with a controlled accessible color legend: full domains resolve before series geometry and focus points are filtered. | +| [80 — Axis pointer](./cases/80-echarts-axis-pointer/example.tsx) | Definition now | Resolved with `focusGuideX`, grouped x focus, native grouped tooltip, and the native color legend; only conformance observation remains outside. | +| [81 — Interactive legend](./cases/81-recharts-interactive-legend/example.tsx) | First-party primitive | Resolved with a controlled accessible color legend: full domains resolve before series geometry and focus points are filtered. | | [82 — Chart/table selection](./cases/82-chart-table-selection/view.tsx) | First-party primitive | Resolved with `keyedSelection` and `whenSelected`; the semantic HTML table, status, clear action, representative-row policy, and persistence remain application or case UI. | | [83 — Focus/context](./cases/83-focus-context-window/view.tsx) | Optional primitive | `brushX`, controlled signals, and `keyedSelection` own the semantic window and detail selection. Two chart hosts are warranted rather than `viewGrid`: detail and overview have independent rows, domains, axes, margins, sizes, and behavior. The range input and four-month policy remain application UI. | | [84 — Pinned nested tooltip](./cases/84-pinned-nested-chart-tooltip/view.tsx) | Definition now | Resolved with sticky pin-only tooltip visibility, portal placement, dismissal, inline pinned state, and adapter body composition; only cohort policy and the nested child remain outside. | -| [85 — Resource lanes](./cases/85-scrollable-resource-lanes/tanstack.ts) | Application boundary | Native interval marks, inferred ID identity, focus, and tooltip own chart semantics. DOM scrolling, fixed rail/legend, auto-scroll, persistent details, and schedule semantics remain application-owned. | +| [85 — Resource lanes](./cases/85-scrollable-resource-lanes/example.tsx) | Application boundary | Native interval marks, inferred ID identity, focus, and tooltip own chart semantics. DOM scrolling, fixed rail/legend, auto-scroll, persistent details, and schedule semantics remain application-owned. | | [86 — Streaming window](./cases/86-streaming-window-preservation/view.tsx) | Application boundary | Resolved: one exported definition bounds rows before scale inference, paints a decorative line plus interactive observations, and owns focus and tooltip. Arrival, viewport policy, controls, and announcements remain application-owned. | -| [87 — Synchronized cursors](./cases/87-echarts-synchronized-cursors/tanstack.ts) | Definition now | Resolved with one `viewGrid`, shared x semantics, grouped focus, child `focusGuideX` marks, decorative lines, and native sticky pinning. Only the live value summary remains application UI. | -| [88 — Free cursor](./cases/88-echarts-free-cursor/tanstack.ts) | First-party primitive | Resolved with `continuousCursor`: final-scale inversion, unsnapped rules, marker and labels, transient pointer/touch previews, controlled pinning and Escape clearing are definition-owned. Semantic sliders and status remain application UI. | -| [89 — Brush selection](./cases/89-brush-range-selection/tanstack.ts) | Optional primitive | Resolved with controlled `brushX`: final-scale candidate mapping, observed-date snapping, reverse normalization, painting, keyboard handles, cancellation, and teardown are definition-owned. The monthly cohort and live financial summary remain application-owned. | -| [90 — Zoomable window](./cases/90-zoomable-time-window/tanstack.ts) | Optional primitive | Resolved with controlled `zoomX`: final-scale inversion, pointer-anchored wheel zoom, drag and horizontal-wheel pan, touch, keyboard, clamping, cancellation, focus-gated wheel capture, and teardown are definition-owned. Visible-row and y-domain policy, status, reset, and persistence remain application-owned. | -| [91 — Playback scrubber](./cases/91-timeline-playback-scrubber/tanstack.ts) | First-party primitive | Resolved with controlled `handleX`: final-scale track, rule and playhead painting, observed-date snapping, pointer and touch capture, cancellation, keyboard slider semantics, and teardown are definition-owned. Playback timing, transport controls, status, and announcements remain application-owned. | -| [92 — Editable range](./cases/92-editable-event-range/tanstack.ts) | First-party primitive | Resolved with the same controlled `handleX`: final-scale end-handle painting, constrained date snapping, semantic lane placement, pointer and touch capture, cancellation, keyboard slider semantics, and teardown are definition-owned. The date input, validation, event constraint, commit state, status, and exact-value alternative remain application-owned. | -| [112 — Entrance motion](./cases/112-motion-entrance/tanstack.ts) | Definition now | Motion is already definition-local; replay and demo settings are shell controls. | -| [113 — Spring updates](./cases/113-motion-updates/tanstack.ts) | Definition now | Keyed enter/update/exit, interruption, and per-datum policy are already declarative; stage controls remain outside. | -| [114 — Spring line](./cases/114-spring-line-motion/tanstack.ts) | Definition now | Line morph and per-series spring overrides are already declarative; demo controls remain outside. | -| [115 — Definition motion](./cases/115-definition-motion/tanstack.ts) | Definition now | Chart, mark, datum, axis, tick, and label motion are already definition-owned. | -| [117 — Focus cursor motion](./cases/117-focus-cursor-motion/tanstack.ts) | First-party primitive | Add a stable-key focus guide with x/y rules, marker, labels, and normal motion; remove the second SVG and spring loop. | +| [87 — Synchronized cursors](./cases/87-echarts-synchronized-cursors/example.tsx) | Definition now | Resolved with one `viewGrid`, shared x semantics, grouped focus, child `focusGuideX` marks, decorative lines, and native sticky pinning. Only the live value summary remains application UI. | +| [88 — Free cursor](./cases/88-echarts-free-cursor/example.tsx) | First-party primitive | Resolved with `continuousCursor`: final-scale inversion, unsnapped rules, marker and labels, transient pointer/touch previews, controlled pinning and Escape clearing are definition-owned. Semantic sliders and status remain application UI. | +| [89 — Brush selection](./cases/89-brush-range-selection/example.tsx) | Optional primitive | Resolved with controlled `brushX`: final-scale candidate mapping, observed-date snapping, reverse normalization, painting, keyboard handles, cancellation, and teardown are definition-owned. The monthly cohort and live financial summary remain application-owned. | +| [90 — Zoomable window](./cases/90-zoomable-time-window/example.tsx) | Optional primitive | Resolved with controlled `zoomX`: final-scale inversion, pointer-anchored wheel zoom, drag and horizontal-wheel pan, touch, keyboard, clamping, cancellation, focus-gated wheel capture, and teardown are definition-owned. Visible-row and y-domain policy, status, reset, and persistence remain application-owned. | +| [91 — Playback scrubber](./cases/91-timeline-playback-scrubber/example.tsx) | First-party primitive | Resolved with controlled `handleX`: final-scale track, rule and playhead painting, observed-date snapping, pointer and touch capture, cancellation, keyboard slider semantics, and teardown are definition-owned. Playback timing, transport controls, status, and announcements remain application-owned. | +| [92 — Editable range](./cases/92-editable-event-range/example.tsx) | First-party primitive | Resolved with the same controlled `handleX`: final-scale end-handle painting, constrained date snapping, semantic lane placement, pointer and touch capture, cancellation, keyboard slider semantics, and teardown are definition-owned. The date input, validation, event constraint, commit state, status, and exact-value alternative remain application-owned. | +| [112 — Entrance motion](./cases/112-motion-entrance/example.tsx) | Definition now | Motion is already definition-local; replay and demo settings are shell controls. | +| [113 — Spring updates](./cases/113-motion-updates/example.tsx) | Definition now | Keyed enter/update/exit, interruption, and per-datum policy are already declarative; stage controls remain outside. | +| [114 — Spring line](./cases/114-spring-line-motion/example.tsx) | Definition now | Line morph and per-series spring overrides are already declarative; demo controls remain outside. | +| [115 — Definition motion](./cases/115-definition-motion/example.tsx) | Definition now | Chart, mark, datum, axis, tick, and label motion are already definition-owned. | +| [117 — Focus cursor motion](./cases/117-focus-cursor-motion/example.tsx) | First-party primitive | Add a stable-key focus guide with x/y rules, marker, labels, and normal motion; remove the second SVG and spring loop. | | [118 — Token calendar](./cases/118-token-usage-calendar/shell.tsx) | First-party primitive | Add per-tick text style, anchor, and offset accessors so the first month label needs no post-render DOM mutation. | -| [119 — Stacked bar cursor](./cases/119-stacked-bar-band-cursor/chart.ts) | First-party primitive | Use renderer-native `crosshair` axes for the categorical band, endpoint rule, labels, grouped focus, and ordinary definition motion; only conformance observation remains outside. | -| [120 — Themed area card](./cases/120-themed-interactive-area/chart.ts) | Definition now | Native area, line, points, gradients, sparse guides, tooltip, and keyed spring policy own the chart; the range selector and card remain application UI. | -| [121 — Active bar dashboard](./cases/121-active-bar-dashboard/chart.ts) | Definition now | Native gradient bars, focus states, band cursor, tooltip, and spring updates own the chart; KPI totals double as application-owned series controls. | -| [122 — KPI sparklines](./cases/122-premium-kpi-sparklines/chart.ts) | Definition now | Three guide-free definitions use ordinary area and line marks, gradients, CSS-variable paint, and keyed spring updates; metric copy and responsive card layout remain application content. | -| [123 — Active donut](./cases/123-active-donut-metric/chart.ts) | Definition now | Native pie allocation, rounded arcs, selected wedge and ring layers, center text, tooltip, and spring motion own the visualization; legend buttons own persistent selection. | -| [124 — Theme matrix](./cases/124-theme-palette-matrix/chart.ts) | Definition now | The same area-and-line definition is rendered through three scoped CSS-variable themes with identical geometry; only the three-card matrix and labels are application composition. | -| [125 — Sales funnel](./cases/125-sales-funnel/tanstack.ts) | Definition now | Derive centered stage boundaries from the ordered raw values, then compose one native `areaX` group per stage with direct labels and stable semantic keys. | -| [126 — Drillable sunburst](./cases/126-drillable-sunburst/tanstack.ts) | Optional primitive | The optional sunburst mark owns focused-root partitioning, bounded visible depth, hierarchy-aware polar motion, and stable node geometry; the application owns selected-root state and the center back control. | -| [127 — shadcn dashboard](./cases/127-shadcn-dashboard/tanstack.ts) | Definition now | Shared native TanStack Charts dashboard definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [133 — shadcn area-axes](./cases/133-shadcn-area-axes/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [134 — shadcn area-default](./cases/134-shadcn-area-default/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [135 — shadcn area-gradient](./cases/135-shadcn-area-gradient/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [136 — shadcn area-icons](./cases/136-shadcn-area-icons/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [137 — shadcn area-interactive](./cases/137-shadcn-area-interactive/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [138 — shadcn area-legend](./cases/138-shadcn-area-legend/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [139 — shadcn area-linear](./cases/139-shadcn-area-linear/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [140 — shadcn area-stacked-expand](./cases/140-shadcn-area-stacked-expand/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [141 — shadcn area-stacked](./cases/141-shadcn-area-stacked/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [142 — shadcn area-step](./cases/142-shadcn-area-step/tanstack.ts) | Definition now | Shared native TanStack Charts area definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [143 — shadcn bar-active](./cases/143-shadcn-bar-active/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [144 — shadcn bar-default](./cases/144-shadcn-bar-default/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [145 — shadcn bar-horizontal](./cases/145-shadcn-bar-horizontal/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [146 — shadcn bar-interactive](./cases/146-shadcn-bar-interactive/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [147 — shadcn bar-label-custom](./cases/147-shadcn-bar-label-custom/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [148 — shadcn bar-label](./cases/148-shadcn-bar-label/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [149 — shadcn bar-mixed](./cases/149-shadcn-bar-mixed/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [128 — shadcn bar-multiple](./cases/128-shadcn-bar-multiple/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [150 — shadcn bar-negative](./cases/150-shadcn-bar-negative/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [151 — shadcn bar-stacked](./cases/151-shadcn-bar-stacked/tanstack.ts) | Definition now | Shared native TanStack Charts bar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [152 — shadcn line-default](./cases/152-shadcn-line-default/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [153 — shadcn line-dots-colors](./cases/153-shadcn-line-dots-colors/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [154 — shadcn line-dots-custom](./cases/154-shadcn-line-dots-custom/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [155 — shadcn line-dots](./cases/155-shadcn-line-dots/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [156 — shadcn line-interactive](./cases/156-shadcn-line-interactive/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [157 — shadcn line-label-custom](./cases/157-shadcn-line-label-custom/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [158 — shadcn line-label](./cases/158-shadcn-line-label/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [159 — shadcn line-linear](./cases/159-shadcn-line-linear/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [160 — shadcn line-multiple](./cases/160-shadcn-line-multiple/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [161 — shadcn line-step](./cases/161-shadcn-line-step/tanstack.ts) | Definition now | Shared native TanStack Charts line definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [162 — shadcn pie-donut-active](./cases/162-shadcn-pie-donut-active/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [129 — shadcn pie-donut-text](./cases/129-shadcn-pie-donut-text/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [163 — shadcn pie-donut](./cases/163-shadcn-pie-donut/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [164 — shadcn pie-interactive](./cases/164-shadcn-pie-interactive/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [165 — shadcn pie-label-custom](./cases/165-shadcn-pie-label-custom/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [166 — shadcn pie-label-list](./cases/166-shadcn-pie-label-list/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [167 — shadcn pie-label](./cases/167-shadcn-pie-label/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [168 — shadcn pie-legend](./cases/168-shadcn-pie-legend/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [169 — shadcn pie-separator-none](./cases/169-shadcn-pie-separator-none/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [170 — shadcn pie-simple](./cases/170-shadcn-pie-simple/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [171 — shadcn pie-stacked](./cases/171-shadcn-pie-stacked/tanstack.ts) | Definition now | Shared native TanStack Charts pie definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [172 — shadcn radar-default](./cases/172-shadcn-radar-default/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [173 — shadcn radar-dots](./cases/173-shadcn-radar-dots/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [174 — shadcn radar-grid-circle-fill](./cases/174-shadcn-radar-grid-circle-fill/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [175 — shadcn radar-grid-circle-no-lines](./cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [176 — shadcn radar-grid-circle](./cases/176-shadcn-radar-grid-circle/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [177 — shadcn radar-grid-custom](./cases/177-shadcn-radar-grid-custom/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [178 — shadcn radar-grid-fill](./cases/178-shadcn-radar-grid-fill/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [179 — shadcn radar-grid-none](./cases/179-shadcn-radar-grid-none/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [180 — shadcn radar-icons](./cases/180-shadcn-radar-icons/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [181 — shadcn radar-label-custom](./cases/181-shadcn-radar-label-custom/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [182 — shadcn radar-legend](./cases/182-shadcn-radar-legend/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [183 — shadcn radar-lines-only](./cases/183-shadcn-radar-lines-only/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [130 — shadcn radar-multiple](./cases/130-shadcn-radar-multiple/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [184 — shadcn radar-radius](./cases/184-shadcn-radar-radius/tanstack.ts) | Definition now | Shared native TanStack Charts radar definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [185 — shadcn radial-grid](./cases/185-shadcn-radial-grid/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [186 — shadcn radial-label](./cases/186-shadcn-radial-label/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [187 — shadcn radial-shape](./cases/187-shadcn-radial-shape/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [188 — shadcn radial-simple](./cases/188-shadcn-radial-simple/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [189 — shadcn radial-stacked](./cases/189-shadcn-radial-stacked/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [131 — shadcn radial-text](./cases/131-shadcn-radial-text/tanstack.ts) | Definition now | Shared native TanStack Charts radial definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [132 — shadcn tooltip-advanced](./cases/132-shadcn-tooltip-advanced/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [190 — shadcn tooltip-default](./cases/190-shadcn-tooltip-default/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [191 — shadcn tooltip-formatter](./cases/191-shadcn-tooltip-formatter/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [192 — shadcn tooltip-icons](./cases/192-shadcn-tooltip-icons/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [193 — shadcn tooltip-indicator-line](./cases/193-shadcn-tooltip-indicator-line/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [194 — shadcn tooltip-indicator-none](./cases/194-shadcn-tooltip-indicator-none/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [195 — shadcn tooltip-label-custom](./cases/195-shadcn-tooltip-label-custom/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [196 — shadcn tooltip-label-formatter](./cases/196-shadcn-tooltip-label-formatter/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | -| [197 — shadcn tooltip-label-none](./cases/197-shadcn-tooltip-label-none/tanstack.ts) | Definition now | Shared native TanStack Charts tooltip definitions reproduce the pinned shadcn variant; the card remains application presentation. | +| [119 — Stacked bar cursor](./cases/119-stacked-bar-band-cursor/example.tsx) | First-party primitive | Use renderer-native `crosshair` axes for the categorical band, endpoint rule, labels, grouped focus, and ordinary definition motion; only conformance observation remains outside. | +| [120 — Themed area card](./cases/120-themed-interactive-area/example.tsx) | Definition now | Native area, line, points, gradients, sparse guides, tooltip, and keyed spring policy own the chart; the range selector and card remain application UI. | +| [121 — Active bar dashboard](./cases/121-active-bar-dashboard/example.tsx) | Definition now | Native gradient bars, focus states, band cursor, tooltip, and spring updates own the chart; KPI totals double as application-owned series controls. | +| [122 — KPI sparklines](./cases/122-premium-kpi-sparklines/example.tsx) | Definition now | Three guide-free definitions use ordinary area and line marks, gradients, CSS-variable paint, and keyed spring updates; metric copy and responsive card layout remain application content. | +| [123 — Active donut](./cases/123-active-donut-metric/example.tsx) | Definition now | Native pie allocation, rounded arcs, selected wedge and ring layers, center text, tooltip, and spring motion own the visualization; legend buttons own persistent selection. | +| [124 — Theme matrix](./cases/124-theme-palette-matrix/example.tsx) | Definition now | The same area-and-line definition is rendered through three scoped CSS-variable themes with identical geometry; only the three-card matrix and labels are application composition. | +| [125 — Sales funnel](./cases/125-sales-funnel/example.tsx) | Definition now | Derive centered stage boundaries from the ordered raw values, then compose one native `areaX` group per stage with direct labels and stable semantic keys. | +| [126 — Drillable sunburst](./cases/126-drillable-sunburst/example.tsx) | Optional primitive | The optional sunburst mark owns focused-root partitioning, bounded visible depth, hierarchy-aware polar motion, and stable node geometry; the application owns selected-root state and the center back control. | +| [127 — shadcn dashboard](./cases/127-shadcn-dashboard/example.tsx) | Definition now | The self-contained native TanStack Charts dashboard example reproduces the pinned shadcn variant and card presentation. | +| [133 — shadcn area-axes](./cases/133-shadcn-area-axes/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [134 — shadcn area-default](./cases/134-shadcn-area-default/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [135 — shadcn area-gradient](./cases/135-shadcn-area-gradient/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [136 — shadcn area-icons](./cases/136-shadcn-area-icons/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [137 — shadcn area-interactive](./cases/137-shadcn-area-interactive/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [138 — shadcn area-legend](./cases/138-shadcn-area-legend/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [139 — shadcn area-linear](./cases/139-shadcn-area-linear/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [140 — shadcn area-stacked-expand](./cases/140-shadcn-area-stacked-expand/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [141 — shadcn area-stacked](./cases/141-shadcn-area-stacked/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [142 — shadcn area-step](./cases/142-shadcn-area-step/example.tsx) | Definition now | The self-contained native TanStack Charts area example reproduces the pinned shadcn variant and card presentation. | +| [143 — shadcn bar-active](./cases/143-shadcn-bar-active/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [144 — shadcn bar-default](./cases/144-shadcn-bar-default/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [145 — shadcn bar-horizontal](./cases/145-shadcn-bar-horizontal/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [146 — shadcn bar-interactive](./cases/146-shadcn-bar-interactive/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [147 — shadcn bar-label-custom](./cases/147-shadcn-bar-label-custom/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [148 — shadcn bar-label](./cases/148-shadcn-bar-label/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [149 — shadcn bar-mixed](./cases/149-shadcn-bar-mixed/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [128 — shadcn bar-multiple](./cases/128-shadcn-bar-multiple/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [150 — shadcn bar-negative](./cases/150-shadcn-bar-negative/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [151 — shadcn bar-stacked](./cases/151-shadcn-bar-stacked/example.tsx) | Definition now | The self-contained native TanStack Charts bar example reproduces the pinned shadcn variant and card presentation. | +| [152 — shadcn line-default](./cases/152-shadcn-line-default/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [153 — shadcn line-dots-colors](./cases/153-shadcn-line-dots-colors/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [154 — shadcn line-dots-custom](./cases/154-shadcn-line-dots-custom/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [155 — shadcn line-dots](./cases/155-shadcn-line-dots/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [156 — shadcn line-interactive](./cases/156-shadcn-line-interactive/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [157 — shadcn line-label-custom](./cases/157-shadcn-line-label-custom/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [158 — shadcn line-label](./cases/158-shadcn-line-label/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [159 — shadcn line-linear](./cases/159-shadcn-line-linear/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [160 — shadcn line-multiple](./cases/160-shadcn-line-multiple/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [161 — shadcn line-step](./cases/161-shadcn-line-step/example.tsx) | Definition now | The self-contained native TanStack Charts line example reproduces the pinned shadcn variant and card presentation. | +| [162 — shadcn pie-donut-active](./cases/162-shadcn-pie-donut-active/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [129 — shadcn pie-donut-text](./cases/129-shadcn-pie-donut-text/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [163 — shadcn pie-donut](./cases/163-shadcn-pie-donut/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [164 — shadcn pie-interactive](./cases/164-shadcn-pie-interactive/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [165 — shadcn pie-label-custom](./cases/165-shadcn-pie-label-custom/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [166 — shadcn pie-label-list](./cases/166-shadcn-pie-label-list/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [167 — shadcn pie-label](./cases/167-shadcn-pie-label/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [168 — shadcn pie-legend](./cases/168-shadcn-pie-legend/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [169 — shadcn pie-separator-none](./cases/169-shadcn-pie-separator-none/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [170 — shadcn pie-simple](./cases/170-shadcn-pie-simple/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [171 — shadcn pie-stacked](./cases/171-shadcn-pie-stacked/example.tsx) | Definition now | The self-contained native TanStack Charts pie example reproduces the pinned shadcn variant and card presentation. | +| [172 — shadcn radar-default](./cases/172-shadcn-radar-default/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [173 — shadcn radar-dots](./cases/173-shadcn-radar-dots/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [174 — shadcn radar-grid-circle-fill](./cases/174-shadcn-radar-grid-circle-fill/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [175 — shadcn radar-grid-circle-no-lines](./cases/175-shadcn-radar-grid-circle-no-lines/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [176 — shadcn radar-grid-circle](./cases/176-shadcn-radar-grid-circle/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [177 — shadcn radar-grid-custom](./cases/177-shadcn-radar-grid-custom/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [178 — shadcn radar-grid-fill](./cases/178-shadcn-radar-grid-fill/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [179 — shadcn radar-grid-none](./cases/179-shadcn-radar-grid-none/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [180 — shadcn radar-icons](./cases/180-shadcn-radar-icons/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [181 — shadcn radar-label-custom](./cases/181-shadcn-radar-label-custom/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [182 — shadcn radar-legend](./cases/182-shadcn-radar-legend/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [183 — shadcn radar-lines-only](./cases/183-shadcn-radar-lines-only/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [130 — shadcn radar-multiple](./cases/130-shadcn-radar-multiple/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [184 — shadcn radar-radius](./cases/184-shadcn-radar-radius/example.tsx) | Definition now | The self-contained native TanStack Charts radar example reproduces the pinned shadcn variant and card presentation. | +| [185 — shadcn radial-grid](./cases/185-shadcn-radial-grid/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [186 — shadcn radial-label](./cases/186-shadcn-radial-label/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [187 — shadcn radial-shape](./cases/187-shadcn-radial-shape/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [188 — shadcn radial-simple](./cases/188-shadcn-radial-simple/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [189 — shadcn radial-stacked](./cases/189-shadcn-radial-stacked/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [131 — shadcn radial-text](./cases/131-shadcn-radial-text/example.tsx) | Definition now | The self-contained native TanStack Charts radial example reproduces the pinned shadcn variant and card presentation. | +| [132 — shadcn tooltip-advanced](./cases/132-shadcn-tooltip-advanced/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [190 — shadcn tooltip-default](./cases/190-shadcn-tooltip-default/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [191 — shadcn tooltip-formatter](./cases/191-shadcn-tooltip-formatter/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [192 — shadcn tooltip-icons](./cases/192-shadcn-tooltip-icons/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [193 — shadcn tooltip-indicator-line](./cases/193-shadcn-tooltip-indicator-line/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [194 — shadcn tooltip-indicator-none](./cases/194-shadcn-tooltip-indicator-none/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [195 — shadcn tooltip-label-custom](./cases/195-shadcn-tooltip-label-custom/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [196 — shadcn tooltip-label-formatter](./cases/196-shadcn-tooltip-label-formatter/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | +| [197 — shadcn tooltip-label-none](./cases/197-shadcn-tooltip-label-none/example.tsx) | Definition now | The self-contained native TanStack Charts tooltip example reproduces the pinned shadcn variant and card presentation. | ## Reference evidence diff --git a/benchmarks/conformance/DEFINITION-COVERAGE-OVERVIEW.md b/benchmarks/conformance/DEFINITION-COVERAGE-OVERVIEW.md index 45f712e3..48840901 100644 --- a/benchmarks/conformance/DEFINITION-COVERAGE-OVERVIEW.md +++ b/benchmarks/conformance/DEFINITION-COVERAGE-OVERVIEW.md @@ -80,191 +80,191 @@ presentation policy. | Case | Disposition | Current boundary | Case-local evidence | | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------- | -| [01-line-gaps — Apple stock line with seasonal gaps](./cases/01-line-gaps/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [02-multi-line-end-labels — Industry unemployment with end labels](./cases/02-multi-line-end-labels/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [03-temperature-range-band — San Francisco temperature range band](./cases/03-temperature-range-band/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [04-stacked-time-area — Industry unemployment stacked area](./cases/04-stacked-time-area/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [13-interval-timeline — Open-to-close price intervals](./cases/13-interval-timeline/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [14-error-bars — Point estimates with error bars](./cases/14-error-bars/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [15-boxplot — Grouped boxplot](./cases/15-boxplot/tanstack.ts) | First-party primitive | `composite-mark`, `box-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [16-lollipop — Ranked lollipop chart](./cases/16-lollipop/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [17-dumbbell — Dumbbell comparison](./cases/17-dumbbell/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [18-cumulative-histogram — Cumulative histogram](./cases/18-cumulative-histogram/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [19-moving-average-line — San Francisco temperature moving averages](./cases/19-moving-average-line/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [20-normalized-stacked-area — Industry share of unemployment](./cases/20-normalized-stacked-area/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [21-streamgraph — Streamgraph](./cases/21-streamgraph/tanstack.ts) | First-party primitive | `inside-out-wiggle-stack` | `tanstack.ts`, `tanstack.test.ts` | -| [22-bollinger-band — Bollinger band](./cases/22-bollinger-band/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [24-quantitative-binned-heatmap — Quantitative binned heatmap](./cases/24-quantitative-binned-heatmap/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [25-calendar-heatmap — Precipitation calendar heatmap](./cases/25-calendar-heatmap/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [26-diverging-likert — Diverging Likert responses](./cases/26-diverging-likert/tanstack.ts) | First-party primitive | `anchored-stack` | `tanstack.ts`, `tanstack.test.ts` | +| [01-line-gaps — Apple stock line with seasonal gaps](./cases/01-line-gaps/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [02-multi-line-end-labels — Industry unemployment with end labels](./cases/02-multi-line-end-labels/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [03-temperature-range-band — San Francisco temperature range band](./cases/03-temperature-range-band/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [04-stacked-time-area — Industry unemployment stacked area](./cases/04-stacked-time-area/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [13-interval-timeline — Open-to-close price intervals](./cases/13-interval-timeline/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [14-error-bars — Point estimates with error bars](./cases/14-error-bars/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [15-boxplot — Grouped boxplot](./cases/15-boxplot/example.tsx) | First-party primitive | `composite-mark`, `box-mark` | `example.tsx`, `tanstack.test.ts` | +| [16-lollipop — Ranked lollipop chart](./cases/16-lollipop/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [17-dumbbell — Dumbbell comparison](./cases/17-dumbbell/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [18-cumulative-histogram — Cumulative histogram](./cases/18-cumulative-histogram/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [19-moving-average-line — San Francisco temperature moving averages](./cases/19-moving-average-line/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [20-normalized-stacked-area — Industry share of unemployment](./cases/20-normalized-stacked-area/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [21-streamgraph — Streamgraph](./cases/21-streamgraph/example.tsx) | First-party primitive | `inside-out-wiggle-stack` | `example.tsx`, `tanstack.test.ts` | +| [22-bollinger-band — Bollinger band](./cases/22-bollinger-band/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [24-quantitative-binned-heatmap — Quantitative binned heatmap](./cases/24-quantitative-binned-heatmap/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [25-calendar-heatmap — Precipitation calendar heatmap](./cases/25-calendar-heatmap/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [26-diverging-likert — Diverging Likert responses](./cases/26-diverging-likert/example.tsx) | First-party primitive | `anchored-stack` | `example.tsx`, `tanstack.test.ts` | | [27-parallel-coordinates — Parallel coordinates](./cases/27-parallel-coordinates/tanstack.test.ts) | First-party primitive | `fold-transform` | `tanstack.test.ts` | -| [28-candlestick — Candlestick chart](./cases/28-candlestick/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [29-waterfall — Annual gasoline-price waterfall](./cases/29-waterfall/tanstack.ts) | First-party primitive | `waterfall-layout` | `tanstack.ts`, `tanstack.test.ts` | +| [28-candlestick — Candlestick chart](./cases/28-candlestick/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [29-waterfall — Annual gasoline-price waterfall](./cases/29-waterfall/example.tsx) | First-party primitive | `waterfall-layout` | `example.tsx`, `tanstack.test.ts` | | [30-slopegraph — Two-period slopegraph](./cases/30-slopegraph/tanstack.test.ts) | First-party primitive | `fold-transform` | `tanstack.test.ts` | -| [31-linear-regression — Scatterplot with linear regression](./cases/31-linear-regression/tanstack.ts) | First-party primitive | `composite-mark`, `line-x-mark`, `regression-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [32-change-arrows — Metro inequality change arrows](./cases/32-change-arrows/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [33-difference-chart — Apple close versus moving average](./cases/33-difference-chart/tanstack.ts) | First-party primitive | `composite-mark`, `line-x-mark`, `resolved-mark-layout`, `difference-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [34-pointer-tooltip — Pointer-selected tooltip](./cases/34-pointer-tooltip/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [35-grouped-tooltip — Grouped X tooltip](./cases/35-grouped-tooltip/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [36-hierarchy-tree — Tidy hierarchy tree](./cases/36-hierarchy-tree/tanstack.ts) | Optional primitive | `hierarchy-tree` | `tanstack.ts` | -| [37-delaunay-network — Delaunay spatial network](./cases/37-delaunay-network/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `spatial-delaunay` | `tanstack.ts` | -| [38-contour-topography — Filled wind-speed contours](./cases/38-contour-topography/tanstack.ts) | Optional primitive | `spatial-contour` | `tanstack.ts` | -| [39-density-contours — Point density contours](./cases/39-density-contours/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `spatial-density` | `tanstack.ts` | -| [40-force-directed-network — Les Misérables character network](./cases/40-force-directed-network/tanstack.ts) | Optional primitive | `network-force` | `tanstack.ts` | -| [40-geojson-map — Westport House floor plan](./cases/40-geojson-map/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [41-waffle-unit-chart — English letter frequency waffle](./cases/41-waffle-unit-chart/tanstack.ts) | First-party primitive | `resolved-mark-layout`, `waffle-mark` | `tanstack.ts` | -| [42-vector-field — Two-dimensional vector field](./cases/42-vector-field/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [43-hexbin-density — Hexagonally binned density](./cases/43-hexbin-density/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `spatial-hexbin` | `tanstack.ts` | -| [44-framed-scatter — Framed guide-free scatterplot](./cases/44-framed-scatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [50-empirical-cdf — Empirical cumulative distribution](./cases/50-empirical-cdf/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [51-faceted-distributions — Faceted distributions](./cases/51-faceted-distributions/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [52-beeswarm-dodge — Beeswarm distribution](./cases/52-beeswarm-dodge/tanstack.ts) | First-party primitive | `resolved-mark-layout`, `dodge-layout` | `tanstack.ts` | -| [53-log-scale-scatter — Log-scale Flare class sizes](./cases/53-log-scale-scatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [54-bump-ranking — Industry unemployment bump chart](./cases/54-bump-ranking/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [55-indexed-multi-line — Indexed industry unemployment](./cases/55-indexed-multi-line/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [56-connected-scatter — Directed connected scatterplot](./cases/56-connected-scatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [57-scatter-marginal-histograms — Scatterplot with marginal histograms](./cases/57-scatter-marginal-histograms/tanstack.ts) | First-party primitive | `view-composition` | `tanstack.ts`, `tanstack.test.ts` | -| [58-select-extrema — Annotated AAPL extrema](./cases/58-select-extrema/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [59-grouped-reducer-bars — Mean penguin mass reducer bars](./cases/59-grouped-reducer-bars/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [60-lag-autocorrelation — AAPL lag-one autocorrelation](./cases/60-lag-autocorrelation/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [61-quantile-ribbon — Industry unemployment percentile ribbon](./cases/61-quantile-ribbon/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [62-ridgeline-density — Ridgeline density comparison](./cases/62-ridgeline-density/tanstack.ts) | First-party primitive | `ridgeline-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [63-violin-distributions — Violin distribution comparison](./cases/63-violin-distributions/tanstack.ts) | First-party primitive | `violin-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [64-marimekko-mosaic — Marimekko survey composition](./cases/64-marimekko-mosaic/tanstack.ts) | First-party primitive | `mosaic-layout` | `tanstack.ts`, `tanstack.test.ts` | -| [65-voronoi-nearest-tooltip — Voronoi nearest-point interaction](./cases/65-voronoi-nearest-tooltip/tanstack.ts) | Optional primitive | `spatial-voronoi` | `tanstack.ts` | -| [70-composed-chart — Layered Seattle weather chart](./cases/70-composed-chart/tanstack.ts) | First-party primitive | `bar-max-thickness` | `tanstack.ts`, `tanstack.test.ts` | -| [71-recharts-population-pyramid — Palmer penguins by species and sex](./cases/71-recharts-population-pyramid/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [72-recharts-mixed-bars — Stacked and adjacent Seattle weather bars](./cases/72-recharts-mixed-bars/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [73-many-point-scatter — Automobile specifications scatter](./cases/73-many-point-scatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [74-recharts-treemap — Flare analytics treemap](./cases/74-recharts-treemap/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `hierarchy-treemap` | `tanstack.ts` | +| [31-linear-regression — Scatterplot with linear regression](./cases/31-linear-regression/example.tsx) | First-party primitive | `composite-mark`, `line-x-mark`, `regression-mark` | `example.tsx`, `tanstack.test.ts` | +| [32-change-arrows — Metro inequality change arrows](./cases/32-change-arrows/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [33-difference-chart — Apple close versus moving average](./cases/33-difference-chart/example.tsx) | First-party primitive | `composite-mark`, `line-x-mark`, `resolved-mark-layout`, `difference-mark` | `example.tsx`, `tanstack.test.ts` | +| [34-pointer-tooltip — Pointer-selected tooltip](./cases/34-pointer-tooltip/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [35-grouped-tooltip — Grouped X tooltip](./cases/35-grouped-tooltip/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [36-hierarchy-tree — Tidy hierarchy tree](./cases/36-hierarchy-tree/example.tsx) | Optional primitive | `hierarchy-tree` | `example.tsx` | +| [37-delaunay-network — Delaunay spatial network](./cases/37-delaunay-network/example.tsx) | Optional primitive | `resolved-mark-layout`, `spatial-delaunay` | `example.tsx` | +| [38-contour-topography — Filled wind-speed contours](./cases/38-contour-topography/example.tsx) | Optional primitive | `spatial-contour` | `example.tsx` | +| [39-density-contours — Point density contours](./cases/39-density-contours/example.tsx) | Optional primitive | `resolved-mark-layout`, `spatial-density` | `example.tsx` | +| [40-force-directed-network — Les Misérables character network](./cases/40-force-directed-network/example.tsx) | Optional primitive | `network-force` | `example.tsx` | +| [40-geojson-map — Westport House floor plan](./cases/40-geojson-map/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [41-waffle-unit-chart — English letter frequency waffle](./cases/41-waffle-unit-chart/example.tsx) | First-party primitive | `resolved-mark-layout`, `waffle-mark` | `example.tsx` | +| [42-vector-field — Two-dimensional vector field](./cases/42-vector-field/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [43-hexbin-density — Hexagonally binned density](./cases/43-hexbin-density/example.tsx) | Optional primitive | `resolved-mark-layout`, `spatial-hexbin` | `example.tsx` | +| [44-framed-scatter — Framed guide-free scatterplot](./cases/44-framed-scatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [50-empirical-cdf — Empirical cumulative distribution](./cases/50-empirical-cdf/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [51-faceted-distributions — Faceted distributions](./cases/51-faceted-distributions/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [52-beeswarm-dodge — Beeswarm distribution](./cases/52-beeswarm-dodge/example.tsx) | First-party primitive | `resolved-mark-layout`, `dodge-layout` | `example.tsx` | +| [53-log-scale-scatter — Log-scale Flare class sizes](./cases/53-log-scale-scatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [54-bump-ranking — Industry unemployment bump chart](./cases/54-bump-ranking/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [55-indexed-multi-line — Indexed industry unemployment](./cases/55-indexed-multi-line/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [56-connected-scatter — Directed connected scatterplot](./cases/56-connected-scatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [57-scatter-marginal-histograms — Scatterplot with marginal histograms](./cases/57-scatter-marginal-histograms/example.tsx) | First-party primitive | `view-composition` | `example.tsx`, `tanstack.test.ts` | +| [58-select-extrema — Annotated AAPL extrema](./cases/58-select-extrema/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [59-grouped-reducer-bars — Mean penguin mass reducer bars](./cases/59-grouped-reducer-bars/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [60-lag-autocorrelation — AAPL lag-one autocorrelation](./cases/60-lag-autocorrelation/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [61-quantile-ribbon — Industry unemployment percentile ribbon](./cases/61-quantile-ribbon/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [62-ridgeline-density — Ridgeline density comparison](./cases/62-ridgeline-density/example.tsx) | First-party primitive | `ridgeline-mark` | `example.tsx`, `tanstack.test.ts` | +| [63-violin-distributions — Violin distribution comparison](./cases/63-violin-distributions/example.tsx) | First-party primitive | `violin-mark` | `example.tsx`, `tanstack.test.ts` | +| [64-marimekko-mosaic — Marimekko survey composition](./cases/64-marimekko-mosaic/example.tsx) | First-party primitive | `mosaic-layout` | `example.tsx`, `tanstack.test.ts` | +| [65-voronoi-nearest-tooltip — Voronoi nearest-point interaction](./cases/65-voronoi-nearest-tooltip/example.tsx) | Optional primitive | `spatial-voronoi` | `example.tsx` | +| [70-composed-chart — Layered Seattle weather chart](./cases/70-composed-chart/example.tsx) | First-party primitive | `bar-max-thickness` | `example.tsx`, `tanstack.test.ts` | +| [71-recharts-population-pyramid — Palmer penguins by species and sex](./cases/71-recharts-population-pyramid/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [72-recharts-mixed-bars — Stacked and adjacent Seattle weather bars](./cases/72-recharts-mixed-bars/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [73-many-point-scatter — Automobile specifications scatter](./cases/73-many-point-scatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [74-recharts-treemap — Flare analytics treemap](./cases/74-recharts-treemap/example.tsx) | Optional primitive | `resolved-mark-layout`, `hierarchy-treemap` | `example.tsx` | | [75-radar — Simple radar chart](./cases/75-radar/tanstack.test.ts) | First-party primitive | `fold-transform` | `tanstack.test.ts` | | [76-pie — English letter frequency pie](./cases/76-pie/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | | [77-donut — English letter frequency donut](./cases/77-donut/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | | [78-gauge — Survey agreement share gauge](./cases/78-gauge/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | -| [80-echarts-axis-pointer — Snapped axis pointer with grouped tooltip](./cases/80-echarts-axis-pointer/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [81-recharts-interactive-legend — Interactive series legend](./cases/81-recharts-interactive-legend/tanstack.ts) | First-party primitive | `controlled-signals`, `interactive-legend` | `tanstack.ts`, `tanstack.test.ts`, `view.tsx` | -| [82-chart-table-selection — Linked chart and data table selection](./cases/82-chart-table-selection/tanstack.ts) | First-party primitive | `controlled-signals`, `keyed-selection` | `tanstack.ts`, `tanstack.test.ts`, `view.tsx` | -| [83-focus-context-window — Focus and context time window](./cases/83-focus-context-window/tanstack.ts) | Optional primitive | `controlled-signals`, `keyed-selection`, `configured-scale-inversion`, `brush-x` | `tanstack.ts`, `tanstack.test.ts`, `view.tsx` | -| [84-pinned-nested-chart-tooltip — Pinned penguin tooltip with a nested chart](./cases/84-pinned-nested-chart-tooltip/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `model.test.ts`, `view.tsx` | -| [85-scrollable-resource-lanes — Scrollable resource timeline lanes](./cases/85-scrollable-resource-lanes/tanstack.ts) | Application boundary | `application-boundary` | `tanstack.ts`, `tanstack.test.ts`, `shell.ts` | +| [80-echarts-axis-pointer — Snapped axis pointer with grouped tooltip](./cases/80-echarts-axis-pointer/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [81-recharts-interactive-legend — Interactive series legend](./cases/81-recharts-interactive-legend/example.tsx) | First-party primitive | `controlled-signals`, `interactive-legend` | `example.tsx`, `tanstack.test.ts`, `view.tsx` | +| [82-chart-table-selection — Linked chart and data table selection](./cases/82-chart-table-selection/example.tsx) | First-party primitive | `controlled-signals`, `keyed-selection` | `example.tsx`, `tanstack.test.ts`, `view.tsx` | +| [83-focus-context-window — Focus and context time window](./cases/83-focus-context-window/example.tsx) | Optional primitive | `controlled-signals`, `keyed-selection`, `configured-scale-inversion`, `brush-x` | `example.tsx`, `tanstack.test.ts`, `view.tsx` | +| [84-pinned-nested-chart-tooltip — Pinned penguin tooltip with a nested chart](./cases/84-pinned-nested-chart-tooltip/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `model.test.ts`, `view.tsx` | +| [85-scrollable-resource-lanes — Scrollable resource timeline lanes](./cases/85-scrollable-resource-lanes/example.tsx) | Application boundary | `application-boundary` | `example.tsx`, `tanstack.test.ts`, `shell.ts` | | [86-streaming-window-preservation — Streaming window preservation](./cases/86-streaming-window-preservation/data.test.ts) | Application boundary | `application-boundary`, `decorative-mark` | `data.test.ts`, `tanstack.test.ts` | -| [87-echarts-synchronized-cursors — Synchronized cursors across views](./cases/87-echarts-synchronized-cursors/tanstack.ts) | Definition now | `view-composition`, `focus-guide`, `decorative-mark` | `tanstack.ts`, `tanstack.test.ts` | -| [88-echarts-free-cursor — Free cursor over car measurements](./cases/88-echarts-free-cursor/tanstack.ts) | First-party primitive | `controlled-signals`, `configured-scale-inversion`, `continuous-cursor` | `tanstack.ts`, `tanstack.test.ts` | -| [89-brush-range-selection — Observed monthly AAPL brush selection](./cases/89-brush-range-selection/tanstack.ts) | Optional primitive | `controlled-signals`, `configured-scale-inversion`, `brush-x` | `tanstack.ts`, `tanstack.test.ts` | -| [90-zoomable-time-window — Wheel zoom and pan over AAPL closes](./cases/90-zoomable-time-window/case.json) | Optional primitive | `controlled-signals`, `configured-scale-inversion`, `zoom-x` | `case.json`, `tanstack.ts`, `tanstack.test.ts` | -| [91-timeline-playback-scrubber — AAPL close playback scrubber](./cases/91-timeline-playback-scrubber/tanstack.ts) | First-party primitive | `controlled-signals`, `scale-handle` | `tanstack.ts`, `tanstack.test.ts` | -| [92-editable-event-range — Editable event range](./cases/92-editable-event-range/tanstack.ts) | First-party primitive | `controlled-signals`, `scale-handle` | `tanstack.ts`, `tanstack.test.ts` | +| [87-echarts-synchronized-cursors — Synchronized cursors across views](./cases/87-echarts-synchronized-cursors/example.tsx) | Definition now | `view-composition`, `focus-guide`, `decorative-mark` | `example.tsx`, `tanstack.test.ts` | +| [88-echarts-free-cursor — Free cursor over car measurements](./cases/88-echarts-free-cursor/example.tsx) | First-party primitive | `controlled-signals`, `configured-scale-inversion`, `continuous-cursor` | `example.tsx`, `tanstack.test.ts` | +| [89-brush-range-selection — Observed monthly AAPL brush selection](./cases/89-brush-range-selection/example.tsx) | Optional primitive | `controlled-signals`, `configured-scale-inversion`, `brush-x` | `example.tsx`, `tanstack.test.ts` | +| [90-zoomable-time-window — Wheel zoom and pan over AAPL closes](./cases/90-zoomable-time-window/case.json) | Optional primitive | `controlled-signals`, `configured-scale-inversion`, `zoom-x` | `case.json`, `example.tsx`, `tanstack.test.ts` | +| [91-timeline-playback-scrubber — AAPL close playback scrubber](./cases/91-timeline-playback-scrubber/example.tsx) | First-party primitive | `controlled-signals`, `scale-handle` | `example.tsx`, `tanstack.test.ts` | +| [92-editable-event-range — Editable event range](./cases/92-editable-event-range/example.tsx) | First-party primitive | `controlled-signals`, `scale-handle` | `example.tsx`, `tanstack.test.ts` | | [93-labeled-pie — Letter frequency pie with labels](./cases/93-labeled-pie/tanstack.test.ts) | First-party primitive | `polar-value-allocation`, `radial-labels` | `tanstack.test.ts` | | [94-center-donut — Letter frequency donut with total](./cases/94-center-donut/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | | [95-rounded-donut — Rounded letter frequency donut](./cases/95-rounded-donut/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | | [96-nested-donut — Nested Flare package sizes](./cases/96-nested-donut/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | -| [97-rose — English letter frequency rose](./cases/97-rose/tanstack.ts) | First-party primitive | `radial-bars` | `tanstack.ts`, `tanstack.test.ts` | +| [97-rose — English letter frequency rose](./cases/97-rose/example.tsx) | First-party primitive | `radial-bars` | `example.tsx`, `tanstack.test.ts` | | [98-needle-gauge — County unemployment gauge](./cases/98-needle-gauge/tanstack.test.ts) | First-party primitive | `polar-value-allocation` | `tanstack.test.ts` | | [99-comparative-radar — Comparative radar chart](./cases/99-comparative-radar/tanstack.test.ts) | First-party primitive | `fold-transform` | `tanstack.test.ts` | | [100-radial-bars — Concentric letter frequency bars](./cases/100-radial-bars/tanstack.test.ts) | First-party primitive | `radial-bars` | `tanstack.test.ts` | -| [101-sunburst — Flare analytics sunburst](./cases/101-sunburst/tanstack.ts) | Optional primitive | `hierarchy-sunburst` | `tanstack.ts`, `tanstack.test.ts` | -| [102-world-choropleth — World learning-poverty choropleth](./cases/102-world-choropleth/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [103-bubble-map — World population bubble map](./cases/103-bubble-map/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [104-orthographic-globe — Orthographic globe with graticule](./cases/104-orthographic-globe/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [105-route-map — HMS Beagle voyage](./cases/105-route-map/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [106-polar-line — Seattle daily high-temperature polar line](./cases/106-polar-line/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [107-polar-scatter — Surface wind polar scatter](./cases/107-polar-scatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [108-country-choropleth — World population-density choropleth](./cases/108-country-choropleth/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [109-us-state-choropleth — United States county unemployment choropleth](./cases/109-us-state-choropleth/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [110-projection-gallery — Standard world projection gallery](./cases/110-projection-gallery/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [111-basic-sankey — Basic Sankey](./cases/111-basic-sankey/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `network-sankey` | `tanstack.ts`, `tanstack.test.ts` | -| [111-sankey-flow — Sankey](./cases/111-sankey-flow/tanstack.ts) | Optional primitive | `resolved-mark-layout`, `network-sankey` | `tanstack.ts`, `tanstack.test.ts` | -| [112-motion-entrance — Staggered entrance motion](./cases/112-motion-entrance/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [113-motion-updates — Interrupted spring updates](./cases/113-motion-updates/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [114-spring-line-motion — Spring line updates](./cases/114-spring-line-motion/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [115-definition-motion — Definition-owned motion](./cases/115-definition-motion/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `tanstack.test.ts` | -| [116-geometry-morph — Cross-chart geometry morph](./cases/116-geometry-morph/tanstack.ts) | Inline custom mark | `inline-custom-boundary` | `tanstack.ts`, `tanstack.test.ts` | -| [117-focus-cursor-motion — Focus and crosshair motion](./cases/117-focus-cursor-motion/tanstack.ts) | First-party primitive | `focus-guide` | `tanstack.ts`, `tanstack.test.ts` | -| [118-token-usage-calendar — Token use calendar heatmap](./cases/118-token-usage-calendar/tanstack.ts) | First-party primitive | `axis-tick-label-accessors` | `tanstack.ts`, `tanstack.test.ts`, `shell.test.ts` | -| [119-stacked-bar-band-cursor — Stacked bars with band and rule cursors](./cases/119-stacked-bar-band-cursor/chart.ts) | First-party primitive | `crosshair` | `chart.ts`, `model.test.ts`, `case.json` | -| [120-themed-interactive-area — Themed interactive area card](./cases/120-themed-interactive-area/chart.ts) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.ts`, `tanstack.test.ts` | -| [121-active-bar-dashboard — Active bar dashboard](./cases/121-active-bar-dashboard/chart.ts) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | -| [122-premium-kpi-sparklines — Premium KPI sparklines](./cases/122-premium-kpi-sparklines/chart.ts) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | -| [123-active-donut-metric — Active donut metric](./cases/123-active-donut-metric/chart.ts) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | -| [124-theme-palette-matrix — Theme palette matrix](./cases/124-theme-palette-matrix/chart.ts) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | -| [125-sales-funnel — Sales conversion funnel](./cases/125-sales-funnel/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `model.test.ts`, `case.json` | -| [126-drillable-sunburst — Drillable Flare sunburst](./cases/126-drillable-sunburst/tanstack.ts) | Optional primitive | `hierarchy-sunburst` | `tanstack.ts`, `tanstack.test.ts`, `model.test.ts` | -| [127-shadcn-dashboard — Dashboard](./cases/127-shadcn-dashboard/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [133-shadcn-area-axes — Area Chart - Axes](./cases/133-shadcn-area-axes/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [134-shadcn-area-default — Area Chart](./cases/134-shadcn-area-default/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [135-shadcn-area-gradient — Area Chart - Gradient](./cases/135-shadcn-area-gradient/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [136-shadcn-area-icons — Area Chart - Icons](./cases/136-shadcn-area-icons/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [137-shadcn-area-interactive — Area Chart - Interactive](./cases/137-shadcn-area-interactive/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [138-shadcn-area-legend — Area Chart - Legend](./cases/138-shadcn-area-legend/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [139-shadcn-area-linear — Area Chart - Linear](./cases/139-shadcn-area-linear/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [140-shadcn-area-stacked-expand — Area Chart - Stacked Expanded](./cases/140-shadcn-area-stacked-expand/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [141-shadcn-area-stacked — Area Chart - Stacked](./cases/141-shadcn-area-stacked/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [142-shadcn-area-step — Area Chart - Step](./cases/142-shadcn-area-step/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [143-shadcn-bar-active — Bar Chart - Active](./cases/143-shadcn-bar-active/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [144-shadcn-bar-default — Bar Chart](./cases/144-shadcn-bar-default/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [145-shadcn-bar-horizontal — Bar Chart - Horizontal](./cases/145-shadcn-bar-horizontal/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [146-shadcn-bar-interactive — Bar Chart - Interactive](./cases/146-shadcn-bar-interactive/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [147-shadcn-bar-label-custom — Bar Chart - Custom Label](./cases/147-shadcn-bar-label-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [148-shadcn-bar-label — Bar Chart - Label](./cases/148-shadcn-bar-label/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [149-shadcn-bar-mixed — Bar Chart - Mixed](./cases/149-shadcn-bar-mixed/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [128-shadcn-bar-multiple — Bar Chart - Multiple](./cases/128-shadcn-bar-multiple/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [150-shadcn-bar-negative — Bar Chart - Negative](./cases/150-shadcn-bar-negative/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [151-shadcn-bar-stacked — Bar Chart - Stacked + Legend](./cases/151-shadcn-bar-stacked/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [152-shadcn-line-default — Line Chart](./cases/152-shadcn-line-default/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [153-shadcn-line-dots-colors — Line Chart - Dots Colors](./cases/153-shadcn-line-dots-colors/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [154-shadcn-line-dots-custom — Line Chart - Custom Dots](./cases/154-shadcn-line-dots-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [155-shadcn-line-dots — Line Chart - Dots](./cases/155-shadcn-line-dots/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [156-shadcn-line-interactive — Line Chart - Interactive](./cases/156-shadcn-line-interactive/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [157-shadcn-line-label-custom — Line Chart - Custom Label](./cases/157-shadcn-line-label-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [158-shadcn-line-label — Line Chart - Label](./cases/158-shadcn-line-label/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [159-shadcn-line-linear — Line Chart - Linear](./cases/159-shadcn-line-linear/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [160-shadcn-line-multiple — Line Chart - Multiple](./cases/160-shadcn-line-multiple/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [161-shadcn-line-step — Line Chart - Step](./cases/161-shadcn-line-step/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [162-shadcn-pie-donut-active — Pie Chart - Donut Active](./cases/162-shadcn-pie-donut-active/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [129-shadcn-pie-donut-text — Pie Chart - Donut with Text](./cases/129-shadcn-pie-donut-text/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [163-shadcn-pie-donut — Pie Chart - Donut](./cases/163-shadcn-pie-donut/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [164-shadcn-pie-interactive — Pie Chart - Interactive](./cases/164-shadcn-pie-interactive/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [165-shadcn-pie-label-custom — Pie Chart - Custom Label](./cases/165-shadcn-pie-label-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [166-shadcn-pie-label-list — Pie Chart - Label List](./cases/166-shadcn-pie-label-list/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [167-shadcn-pie-label — Pie Chart - Label](./cases/167-shadcn-pie-label/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [168-shadcn-pie-legend — Pie Chart - Legend](./cases/168-shadcn-pie-legend/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [169-shadcn-pie-separator-none — Pie Chart - Separator None](./cases/169-shadcn-pie-separator-none/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [170-shadcn-pie-simple — Pie Chart](./cases/170-shadcn-pie-simple/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [171-shadcn-pie-stacked — Pie Chart - Stacked](./cases/171-shadcn-pie-stacked/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [172-shadcn-radar-default — Radar Chart](./cases/172-shadcn-radar-default/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [173-shadcn-radar-dots — Radar Chart - Dots](./cases/173-shadcn-radar-dots/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [174-shadcn-radar-grid-circle-fill — Radar Chart - Grid Circle Filled](./cases/174-shadcn-radar-grid-circle-fill/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [175-shadcn-radar-grid-circle-no-lines — Radar Chart - Grid Circle - No lines](./cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [176-shadcn-radar-grid-circle — Radar Chart - Grid Circle](./cases/176-shadcn-radar-grid-circle/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [177-shadcn-radar-grid-custom — Radar Chart - Grid Custom](./cases/177-shadcn-radar-grid-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [178-shadcn-radar-grid-fill — Radar Chart - Grid Filled](./cases/178-shadcn-radar-grid-fill/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [179-shadcn-radar-grid-none — Radar Chart - Grid None](./cases/179-shadcn-radar-grid-none/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [180-shadcn-radar-icons — Radar Chart - Icons](./cases/180-shadcn-radar-icons/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [181-shadcn-radar-label-custom — Radar Chart - Custom Label](./cases/181-shadcn-radar-label-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [182-shadcn-radar-legend — Radar Chart - Legend](./cases/182-shadcn-radar-legend/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [183-shadcn-radar-lines-only — Radar Chart - Lines Only](./cases/183-shadcn-radar-lines-only/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [130-shadcn-radar-multiple — Radar Chart - Multiple](./cases/130-shadcn-radar-multiple/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [184-shadcn-radar-radius — Radar Chart - Radius Axis](./cases/184-shadcn-radar-radius/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [185-shadcn-radial-grid — Radial Chart - Grid](./cases/185-shadcn-radial-grid/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [186-shadcn-radial-label — Radial Chart - Label](./cases/186-shadcn-radial-label/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [187-shadcn-radial-shape — Radial Chart - Shape](./cases/187-shadcn-radial-shape/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [188-shadcn-radial-simple — Radial Chart](./cases/188-shadcn-radial-simple/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [189-shadcn-radial-stacked — Radial Chart - Stacked](./cases/189-shadcn-radial-stacked/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [131-shadcn-radial-text — Radial Chart - Text](./cases/131-shadcn-radial-text/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [132-shadcn-tooltip-advanced — Tooltip - Advanced](./cases/132-shadcn-tooltip-advanced/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [190-shadcn-tooltip-default — Tooltip - Default](./cases/190-shadcn-tooltip-default/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [191-shadcn-tooltip-formatter — Tooltip - Formatter](./cases/191-shadcn-tooltip-formatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [192-shadcn-tooltip-icons — Tooltip - Icons](./cases/192-shadcn-tooltip-icons/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [193-shadcn-tooltip-indicator-line — Tooltip - Line Indicator](./cases/193-shadcn-tooltip-indicator-line/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [194-shadcn-tooltip-indicator-none — Tooltip - No Indicator](./cases/194-shadcn-tooltip-indicator-none/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [195-shadcn-tooltip-label-custom — Tooltip - Custom label](./cases/195-shadcn-tooltip-label-custom/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [196-shadcn-tooltip-label-formatter — Tooltip - Label Formatter](./cases/196-shadcn-tooltip-label-formatter/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [197-shadcn-tooltip-label-none — Tooltip - No Label](./cases/197-shadcn-tooltip-label-none/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts`, `case.json` | -| [bar-grouped — Grouped bars](./cases/bar-grouped/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [bar-horizontal-ranking — Horizontal ranking with long labels](./cases/bar-horizontal-ranking/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [bar-stacked — Stacked bars](./cases/bar-stacked/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [bar-vertical-sorted — Sorted vertical bars](./cases/bar-vertical-sorted/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [facets-anscombe — Anscombe quartet small multiples](./cases/facets-anscombe/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [heatmap-labeled — Labeled ordinal heatmap](./cases/heatmap-labeled/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [histogram — Histogram](./cases/histogram/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | -| [scatter-bubble — Bubble scatterplot](./cases/scatter-bubble/tanstack.ts) | Definition now | `current-definition-api` | `tanstack.ts` | +| [101-sunburst — Flare analytics sunburst](./cases/101-sunburst/example.tsx) | Optional primitive | `hierarchy-sunburst` | `example.tsx`, `tanstack.test.ts` | +| [102-world-choropleth — World learning-poverty choropleth](./cases/102-world-choropleth/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [103-bubble-map — World population bubble map](./cases/103-bubble-map/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [104-orthographic-globe — Orthographic globe with graticule](./cases/104-orthographic-globe/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [105-route-map — HMS Beagle voyage](./cases/105-route-map/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [106-polar-line — Seattle daily high-temperature polar line](./cases/106-polar-line/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [107-polar-scatter — Surface wind polar scatter](./cases/107-polar-scatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [108-country-choropleth — World population-density choropleth](./cases/108-country-choropleth/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [109-us-state-choropleth — United States county unemployment choropleth](./cases/109-us-state-choropleth/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [110-projection-gallery — Standard world projection gallery](./cases/110-projection-gallery/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [111-basic-sankey — Basic Sankey](./cases/111-basic-sankey/example.tsx) | Optional primitive | `resolved-mark-layout`, `network-sankey` | `example.tsx`, `tanstack.test.ts` | +| [111-sankey-flow — Sankey](./cases/111-sankey-flow/example.tsx) | Optional primitive | `resolved-mark-layout`, `network-sankey` | `example.tsx`, `tanstack.test.ts` | +| [112-motion-entrance — Staggered entrance motion](./cases/112-motion-entrance/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [113-motion-updates — Interrupted spring updates](./cases/113-motion-updates/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [114-spring-line-motion — Spring line updates](./cases/114-spring-line-motion/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [115-definition-motion — Definition-owned motion](./cases/115-definition-motion/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `tanstack.test.ts` | +| [116-geometry-morph — Cross-chart geometry morph](./cases/116-geometry-morph/example.tsx) | Inline custom mark | `inline-custom-boundary` | `example.tsx`, `tanstack.test.ts` | +| [117-focus-cursor-motion — Focus and crosshair motion](./cases/117-focus-cursor-motion/example.tsx) | First-party primitive | `focus-guide` | `example.tsx`, `tanstack.test.ts` | +| [118-token-usage-calendar — Token use calendar heatmap](./cases/118-token-usage-calendar/example.tsx) | First-party primitive | `axis-tick-label-accessors` | `example.tsx`, `tanstack.test.ts`, `shell.test.ts` | +| [119-stacked-bar-band-cursor — Stacked bars with band and rule cursors](./cases/119-stacked-bar-band-cursor/example.tsx) | First-party primitive | `crosshair` | `chart.ts`, `model.test.ts`, `case.json` | +| [120-themed-interactive-area — Themed interactive area card](./cases/120-themed-interactive-area/example.tsx) | Definition now | `current-definition-api` | `chart.ts`, `example.tsx`, `tanstack.test.ts` | +| [121-active-bar-dashboard — Active bar dashboard](./cases/121-active-bar-dashboard/example.tsx) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | +| [122-premium-kpi-sparklines — Premium KPI sparklines](./cases/122-premium-kpi-sparklines/example.tsx) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | +| [123-active-donut-metric — Active donut metric](./cases/123-active-donut-metric/example.tsx) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | +| [124-theme-palette-matrix — Theme palette matrix](./cases/124-theme-palette-matrix/example.tsx) | Definition now | `current-definition-api` | `chart.ts`, `tanstack.test.ts`, `view.tsx` | +| [125-sales-funnel — Sales conversion funnel](./cases/125-sales-funnel/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `model.test.ts`, `case.json` | +| [126-drillable-sunburst — Drillable Flare sunburst](./cases/126-drillable-sunburst/example.tsx) | Optional primitive | `hierarchy-sunburst` | `example.tsx`, `tanstack.test.ts`, `model.test.ts` | +| [127-shadcn-dashboard — Dashboard](./cases/127-shadcn-dashboard/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [133-shadcn-area-axes — Area Chart - Axes](./cases/133-shadcn-area-axes/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [134-shadcn-area-default — Area Chart](./cases/134-shadcn-area-default/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [135-shadcn-area-gradient — Area Chart - Gradient](./cases/135-shadcn-area-gradient/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [136-shadcn-area-icons — Area Chart - Icons](./cases/136-shadcn-area-icons/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [137-shadcn-area-interactive — Area Chart - Interactive](./cases/137-shadcn-area-interactive/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [138-shadcn-area-legend — Area Chart - Legend](./cases/138-shadcn-area-legend/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [139-shadcn-area-linear — Area Chart - Linear](./cases/139-shadcn-area-linear/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [140-shadcn-area-stacked-expand — Area Chart - Stacked Expanded](./cases/140-shadcn-area-stacked-expand/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [141-shadcn-area-stacked — Area Chart - Stacked](./cases/141-shadcn-area-stacked/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [142-shadcn-area-step — Area Chart - Step](./cases/142-shadcn-area-step/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [143-shadcn-bar-active — Bar Chart - Active](./cases/143-shadcn-bar-active/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [144-shadcn-bar-default — Bar Chart](./cases/144-shadcn-bar-default/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [145-shadcn-bar-horizontal — Bar Chart - Horizontal](./cases/145-shadcn-bar-horizontal/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [146-shadcn-bar-interactive — Bar Chart - Interactive](./cases/146-shadcn-bar-interactive/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [147-shadcn-bar-label-custom — Bar Chart - Custom Label](./cases/147-shadcn-bar-label-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [148-shadcn-bar-label — Bar Chart - Label](./cases/148-shadcn-bar-label/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [149-shadcn-bar-mixed — Bar Chart - Mixed](./cases/149-shadcn-bar-mixed/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [128-shadcn-bar-multiple — Bar Chart - Multiple](./cases/128-shadcn-bar-multiple/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [150-shadcn-bar-negative — Bar Chart - Negative](./cases/150-shadcn-bar-negative/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [151-shadcn-bar-stacked — Bar Chart - Stacked + Legend](./cases/151-shadcn-bar-stacked/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [152-shadcn-line-default — Line Chart](./cases/152-shadcn-line-default/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [153-shadcn-line-dots-colors — Line Chart - Dots Colors](./cases/153-shadcn-line-dots-colors/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [154-shadcn-line-dots-custom — Line Chart - Custom Dots](./cases/154-shadcn-line-dots-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [155-shadcn-line-dots — Line Chart - Dots](./cases/155-shadcn-line-dots/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [156-shadcn-line-interactive — Line Chart - Interactive](./cases/156-shadcn-line-interactive/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [157-shadcn-line-label-custom — Line Chart - Custom Label](./cases/157-shadcn-line-label-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [158-shadcn-line-label — Line Chart - Label](./cases/158-shadcn-line-label/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [159-shadcn-line-linear — Line Chart - Linear](./cases/159-shadcn-line-linear/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [160-shadcn-line-multiple — Line Chart - Multiple](./cases/160-shadcn-line-multiple/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [161-shadcn-line-step — Line Chart - Step](./cases/161-shadcn-line-step/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [162-shadcn-pie-donut-active — Pie Chart - Donut Active](./cases/162-shadcn-pie-donut-active/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [129-shadcn-pie-donut-text — Pie Chart - Donut with Text](./cases/129-shadcn-pie-donut-text/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [163-shadcn-pie-donut — Pie Chart - Donut](./cases/163-shadcn-pie-donut/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [164-shadcn-pie-interactive — Pie Chart - Interactive](./cases/164-shadcn-pie-interactive/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [165-shadcn-pie-label-custom — Pie Chart - Custom Label](./cases/165-shadcn-pie-label-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [166-shadcn-pie-label-list — Pie Chart - Label List](./cases/166-shadcn-pie-label-list/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [167-shadcn-pie-label — Pie Chart - Label](./cases/167-shadcn-pie-label/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [168-shadcn-pie-legend — Pie Chart - Legend](./cases/168-shadcn-pie-legend/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [169-shadcn-pie-separator-none — Pie Chart - Separator None](./cases/169-shadcn-pie-separator-none/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [170-shadcn-pie-simple — Pie Chart](./cases/170-shadcn-pie-simple/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [171-shadcn-pie-stacked — Pie Chart - Stacked](./cases/171-shadcn-pie-stacked/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [172-shadcn-radar-default — Radar Chart](./cases/172-shadcn-radar-default/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [173-shadcn-radar-dots — Radar Chart - Dots](./cases/173-shadcn-radar-dots/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [174-shadcn-radar-grid-circle-fill — Radar Chart - Grid Circle Filled](./cases/174-shadcn-radar-grid-circle-fill/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [175-shadcn-radar-grid-circle-no-lines — Radar Chart - Grid Circle - No lines](./cases/175-shadcn-radar-grid-circle-no-lines/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [176-shadcn-radar-grid-circle — Radar Chart - Grid Circle](./cases/176-shadcn-radar-grid-circle/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [177-shadcn-radar-grid-custom — Radar Chart - Grid Custom](./cases/177-shadcn-radar-grid-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [178-shadcn-radar-grid-fill — Radar Chart - Grid Filled](./cases/178-shadcn-radar-grid-fill/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [179-shadcn-radar-grid-none — Radar Chart - Grid None](./cases/179-shadcn-radar-grid-none/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [180-shadcn-radar-icons — Radar Chart - Icons](./cases/180-shadcn-radar-icons/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [181-shadcn-radar-label-custom — Radar Chart - Custom Label](./cases/181-shadcn-radar-label-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [182-shadcn-radar-legend — Radar Chart - Legend](./cases/182-shadcn-radar-legend/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [183-shadcn-radar-lines-only — Radar Chart - Lines Only](./cases/183-shadcn-radar-lines-only/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [130-shadcn-radar-multiple — Radar Chart - Multiple](./cases/130-shadcn-radar-multiple/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [184-shadcn-radar-radius — Radar Chart - Radius Axis](./cases/184-shadcn-radar-radius/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [185-shadcn-radial-grid — Radial Chart - Grid](./cases/185-shadcn-radial-grid/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [186-shadcn-radial-label — Radial Chart - Label](./cases/186-shadcn-radial-label/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [187-shadcn-radial-shape — Radial Chart - Shape](./cases/187-shadcn-radial-shape/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [188-shadcn-radial-simple — Radial Chart](./cases/188-shadcn-radial-simple/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [189-shadcn-radial-stacked — Radial Chart - Stacked](./cases/189-shadcn-radial-stacked/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [131-shadcn-radial-text — Radial Chart - Text](./cases/131-shadcn-radial-text/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [132-shadcn-tooltip-advanced — Tooltip - Advanced](./cases/132-shadcn-tooltip-advanced/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [190-shadcn-tooltip-default — Tooltip - Default](./cases/190-shadcn-tooltip-default/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [191-shadcn-tooltip-formatter — Tooltip - Formatter](./cases/191-shadcn-tooltip-formatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [192-shadcn-tooltip-icons — Tooltip - Icons](./cases/192-shadcn-tooltip-icons/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [193-shadcn-tooltip-indicator-line — Tooltip - Line Indicator](./cases/193-shadcn-tooltip-indicator-line/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [194-shadcn-tooltip-indicator-none — Tooltip - No Indicator](./cases/194-shadcn-tooltip-indicator-none/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [195-shadcn-tooltip-label-custom — Tooltip - Custom label](./cases/195-shadcn-tooltip-label-custom/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [196-shadcn-tooltip-label-formatter — Tooltip - Label Formatter](./cases/196-shadcn-tooltip-label-formatter/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [197-shadcn-tooltip-label-none — Tooltip - No Label](./cases/197-shadcn-tooltip-label-none/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json` | +| [bar-grouped — Grouped bars](./cases/bar-grouped/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [bar-horizontal-ranking — Horizontal ranking with long labels](./cases/bar-horizontal-ranking/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [bar-stacked — Stacked bars](./cases/bar-stacked/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [bar-vertical-sorted — Sorted vertical bars](./cases/bar-vertical-sorted/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [facets-anscombe — Anscombe quartet small multiples](./cases/facets-anscombe/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [heatmap-labeled — Labeled ordinal heatmap](./cases/heatmap-labeled/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [histogram — Histogram](./cases/histogram/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | +| [scatter-bubble — Bubble scatterplot](./cases/scatter-bubble/example.tsx) | Definition now | `current-definition-api` | `example.tsx` | diff --git a/benchmarks/conformance/README.md b/benchmarks/conformance/README.md index 7332b56d..42336422 100644 --- a/benchmarks/conformance/README.md +++ b/benchmarks/conformance/README.md @@ -8,8 +8,9 @@ behavior select that renderer explicitly without creating a second harness. Each case owns: - one typed raw data fixture and intent; +- one self-contained public `example.tsx` with the chart definition; - one isolated reference implementation; -- one isolated TanStack implementation; +- one thin TanStack conformance adapter that imports the public example; - official source provenance; - semantic geometry expectations; - optional renderer-independent interaction scenarios; @@ -58,6 +59,9 @@ pnpm dev:conformance # Validate case metadata, source entries, ordering, and index drift pnpm catalog:index:check +# Validate every public example and its case-local source closure +pnpm catalog:examples:check + # Render the checked-in source-derived catalog previews pnpm catalog:previews @@ -100,8 +104,12 @@ current boundary and case-local evidence for every case. `catalog-index.json` is the checked-in metadata contract for consumers that load source directly from the Charts repository. It contains every parsed -`case.json` field plus the TanStack and reference entry paths. The index does +`case.json` field plus the public `example.tsx` entry path. The index does not embed compiled modules, source closures, previews, datasets, or assets. +Every public entry has a default React export and may import only files from its +own case directory. Conformance code imports the definition from that entry; +the adapter, driver, tests, and comparison renderer are not published to the +docs sandbox. Every case also has a checked-in source-derived preview at `benchmarks/conformance/previews/.svg`. The generator mounts the actual @@ -133,7 +141,7 @@ its source entries from that same revision. The Vite application remains the local authoring and conformance surface at `http://localhost:5194/`. TanStack.com owns the public catalog routes and chrome. -It reads `catalog-index.json`, case source, and the checked-in preview SVGs from +It reads `catalog-index.json`, each self-contained example source closure, and the checked-in preview SVGs from one pinned Charts revision, then runs full examples in the site notebook runtime. The site does not maintain parallel preview implementations or consume a generated catalog branch or published renderable-catalog package. diff --git a/benchmarks/conformance/cases/01-line-gaps/example.tsx b/benchmarks/conformance/cases/01-line-gaps/example.tsx new file mode 100644 index 00000000..6aaa90f7 --- /dev/null +++ b/benchmarks/conformance/cases/01-line-gaps/example.tsx @@ -0,0 +1,48 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { aapl } from '@charts-poc/demo-data/aapl' +import { defineChart, lineY } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' + +export const definition = (input: ExampleOptions) => { + const rows = aapl.slice(Math.abs(input.revision) % 2) + + return defineChart({ + marks: [ + lineY(rows, { + x: 'Date', + y: (row) => (row.Date.getUTCMonth() < 3 ? null : row.Close), + stroke: '#2563eb', + strokeWidth: 2.25, + }), + ], + x: { scale: scaleUtc, axis: { label: 'Week' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Close (USD)' } }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Apple closing price with first-quarter gaps' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/01-line-gaps/tanstack.ts b/benchmarks/conformance/cases/01-line-gaps/tanstack.ts index 61b3f01a..6c029e65 100644 --- a/benchmarks/conformance/cases/01-line-gaps/tanstack.ts +++ b/benchmarks/conformance/cases/01-line-gaps/tanstack.ts @@ -1,33 +1,8 @@ -import { aapl } from '@charts-poc/demo-data/aapl' -import { defineChart, lineY } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import type { ConformanceInput } from '../../types' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => { - const rows = samplePreviewData( - aapl.slice(Math.abs(input.revision) % 2), - input, - 80, - [(row) => row.Date.getTime(), (row) => row.Close], - ) +export * from './example' - return defineChart({ - marks: [ - lineY(rows, { - x: 'Date', - y: (row) => (row.Date.getUTCMonth() < 3 ? null : row.Close), - stroke: '#2563eb', - strokeWidth: 2.25, - }), - ], - x: { scale: scaleUtc, axis: { label: 'Week' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Close (USD)' } }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'Apple closing price with first-quarter gaps', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/02-multi-line-end-labels/example.tsx b/benchmarks/conformance/cases/02-multi-line-end-labels/example.tsx new file mode 100644 index 00000000..ebe6ed51 --- /dev/null +++ b/benchmarks/conformance/cases/02-multi-line-end-labels/example.tsx @@ -0,0 +1,85 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, lineY, select, text } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' +import { selectMultiLineData } from './selection' +import type { MultiLineDatum } from './selection' + +const colors = ['#2563eb', '#ea580c', '#059669'] + +export const multiLineEndLabelsDefinition = ( + rows: readonly MultiLineDatum[], + preview = false, +) => { + const endpoints = select(rows, { + by: 'industry', + value: (datum) => datum.date.getTime(), + select: 'max', + }) + + return defineChart({ + marks: [ + lineY(rows, { + id: 'industry-lines', + x: 'date', + y: 'unemployed', + color: 'industry', + strokeWidth: 2.25, + }), + text(endpoints, { + id: 'industry-end-labels', + x: 'date', + y: 'unemployed', + text: 'industry', + color: 'industry', + anchor: preview ? 'end' : 'start', + dx: preview ? -5 : 5, + fontWeight: 600, + }), + ], + x: { scale: scaleUtc, axis: { label: 'Week' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Unemployed (thousands)' }, + }, + color: { + range: colors, + }, + margin: { right: 112 }, + }) +} + +export const definition = (input: ExampleOptions) => + multiLineEndLabelsDefinition( + selectMultiLineData(industries, input.revision), + input.preview === true, + ) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = + 'Unemployment by industry with direct end labels' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts b/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts index ee67e88a..6c029e65 100644 --- a/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts +++ b/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts @@ -1,65 +1,8 @@ -import { defineChart, lineY, select, text } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import type { ConformanceInput } from '../../types' -import { tanstackCase } from '../../shared/mount' -import { selectMultiLineData } from './selection' -import type { MultiLineDatum } from './selection' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#ea580c', '#059669'] +export * from './example' -export const multiLineEndLabelsDefinition = ( - rows: readonly MultiLineDatum[], - preview = false, -) => { - const endpoints = select(rows, { - by: 'industry', - value: (datum) => datum.date.getTime(), - select: 'max', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - lineY(rows, { - id: 'industry-lines', - x: 'date', - y: 'unemployed', - color: 'industry', - strokeWidth: 2.25, - }), - text(endpoints, { - id: 'industry-end-labels', - x: 'date', - y: 'unemployed', - text: 'industry', - color: 'industry', - anchor: preview ? 'end' : 'start', - dx: preview ? -5 : 5, - fontWeight: 600, - }), - ], - x: { scale: scaleUtc, axis: { label: 'Week' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Unemployed (thousands)' }, - }, - color: { - range: colors, - }, - margin: { right: 112 }, - }) -} - -const definition = (input: ConformanceInput) => - multiLineEndLabelsDefinition( - selectMultiLineData(industries, input.revision), - input.preview === true, - ) - -export const catalogCase = tanstackCase( - definition, - 'Unemployment by industry with direct end labels', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/03-temperature-range-band/example.tsx b/benchmarks/conformance/cases/03-temperature-range-band/example.tsx new file mode 100644 index 00000000..153c2ced --- /dev/null +++ b/benchmarks/conformance/cases/03-temperature-range-band/example.tsx @@ -0,0 +1,64 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaY, defineChart, lineY } from '@tanstack/charts' +import { sfTemperatures } from '@charts-poc/demo-data/sf-temperatures' +import { scaleLinear, scaleUtc } from 'd3-scale' + +export const definition = () => { + const rows = sfTemperatures + return defineChart({ + marks: [ + areaY(rows, { + x: 'date', + y1: 'low', + y2: 'high', + fill: '#60a5fa', + fillOpacity: 0.24, + }), + lineY(rows, { + x: 'date', + y: 'low', + stroke: '#2563eb', + strokeWidth: 1.75, + }), + lineY(rows, { + x: 'date', + y: 'high', + stroke: '#dc2626', + strokeWidth: 1.75, + }), + ], + x: { scale: scaleUtc, axis: { label: 'Week' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Temperature (°F)' } }, + }) +} +export const exampleAriaLabel = + 'San Francisco daily low-to-high temperature range' + +export const createExampleChart = () => + defineChart(definition(), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', + })} · ${datum.low.toLocaleString('en-US', { + maximumFractionDigits: 1, + })}–${datum.high.toLocaleString('en-US', { + maximumFractionDigits: 1, + })} °F`, + }, + }, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts b/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts index c23d528f..352e831c 100644 --- a/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts +++ b/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts @@ -1,57 +1,11 @@ -import { areaY, defineChart, lineY } from '@tanstack/charts' -import { sfTemperatures } from '@charts-poc/demo-data/sf-temperatures' -import { scaleLinear, scaleUtc } from 'd3-scale' -import type { ConformanceInput } from '../../types' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => { - const rows = samplePreviewData(sfTemperatures, input, 80, [ - (row) => row.date.getTime(), - (row) => row.low, - (row) => row.high, - ]) - return defineChart({ - marks: [ - areaY(rows, { - x: 'date', - y1: 'low', - y2: 'high', - fill: '#60a5fa', - fillOpacity: 0.24, - }), - lineY(rows, { - x: 'date', - y: 'low', - stroke: '#2563eb', - strokeWidth: 1.75, - }), - lineY(rows, { - x: 'date', - y: 'high', - stroke: '#dc2626', - strokeWidth: 1.75, - }), - ], - x: { scale: scaleUtc, axis: { label: 'Week' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Temperature (°F)' } }, - }) -} +export * from './example' -export const mount = tanstackMount( - definition, - 'San Francisco daily low-to-high temperature range', - { - format: ({ datum }) => - `${datum.date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - timeZone: 'UTC', - })} · ${datum.low.toLocaleString('en-US', { - maximumFractionDigits: 1, - })}–${datum.high.toLocaleString('en-US', { - maximumFractionDigits: 1, - })} °F`, - }, +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/04-stacked-time-area/example.tsx b/benchmarks/conformance/cases/04-stacked-time-area/example.tsx new file mode 100644 index 00000000..84801bb8 --- /dev/null +++ b/benchmarks/conformance/cases/04-stacked-time-area/example.tsx @@ -0,0 +1,79 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaY, colorLegend, defineChart, ruleY } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' + +const colors = [ + '#4e79a7', + '#f28e2c', + '#e15759', + '#76b7b2', + '#59a14f', + '#edc949', + '#af7aa1', + '#ff9da7', + '#9c755f', + '#bab0ab', +] + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + areaY(industries, { + x: 'date', + y: 'unemployed', + color: 'industry', + fillOpacity: 0.78, + }), + ruleY([0]), + ], + x: { scale: scaleUtc, axis: { label: 'Month' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Unemployed (thousands)' }, + }, + color: { + range: colors, + ...(input.preview === true + ? {} + : { legend: colorLegend({ label: 'Industry' }) }), + }, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Unemployment by industry as stacked areas' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + })} · ${datum.unemployed.toLocaleString('en-US')} thousand unemployed`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts b/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts index a663d67f..6c029e65 100644 --- a/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts +++ b/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts @@ -1,60 +1,8 @@ -import { areaY, colorLegend, defineChart, ruleY } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import type { ConformanceInput } from '../../types' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewSeries } from '../../shared/preview' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#4e79a7', - '#f28e2c', - '#e15759', - '#76b7b2', - '#59a14f', - '#edc949', - '#af7aa1', - '#ff9da7', - '#9c755f', - '#bab0ab', -] +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - areaY( - samplePreviewSeries(industries, input, 32, (row) => row.industry), - { - x: 'date', - y: 'unemployed', - color: 'industry', - fillOpacity: 0.78, - }, - ), - ruleY([0]), - ], - x: { scale: scaleUtc, axis: { label: 'Month' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Unemployed (thousands)' }, - }, - color: { - range: colors, - ...(input.preview === true - ? {} - : { legend: colorLegend({ label: 'Industry' }) }), - }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'Unemployment by industry as stacked areas', - { - format: ({ datum }) => - `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { - month: 'short', - year: 'numeric', - timeZone: 'UTC', - })} · ${datum.unemployed.toLocaleString('en-US')} thousand unemployed`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/100-radial-bars/example.tsx b/benchmarks/conformance/cases/100-radial-bars/example.tsx new file mode 100644 index 00000000..1edfa8bb --- /dev/null +++ b/benchmarks/conformance/cases/100-radial-bars/example.tsx @@ -0,0 +1,73 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { polar, radialBarAngle } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { scaleBand, scaleLinear } from 'd3-scale' +import { selectRadialBarData } from './selection' + +const innerRadiusRatio = 0.2 +const colors = ['#7c3aed', '#0ea5e9', '#14b8a6', '#f59e0b'] +const maximumFrequency = alphabet[0]?.frequency ?? 1 + +export const radialBarsDefinition = (input: ExampleOptions) => { + const data = selectRadialBarData(alphabet, input.revision) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.84, + angle: { + scale: scaleLinear().domain([0, maximumFrequency]), + }, + radius: { + scale: () => + scaleBand().paddingInner(0.38).paddingOuter(0.19), + range: [ + ({ radius }) => radius * innerRadiusRatio, + ({ radius }) => radius, + ], + }, + marks: [ + radialBarAngle(data, { + id: 'letter-bars', + className: 'ts-chart__radial-bars', + angle: 'frequency', + radius: 'letter', + key: 'letter', + color: 'letter', + cornerRadius: 'full', + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Concentric letter frequency bars' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(radialBarsDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/100-radial-bars/tanstack.test.ts b/benchmarks/conformance/cases/100-radial-bars/tanstack.test.ts index f91d8e78..988d1b71 100644 --- a/benchmarks/conformance/cases/100-radial-bars/tanstack.test.ts +++ b/benchmarks/conformance/cases/100-radial-bars/tanstack.test.ts @@ -114,7 +114,7 @@ describe('native concentric radial bars', () => { process.cwd(), 'benchmarks/conformance/cases/100-radial-bars', ) - const source = readFileSync(resolve(caseDirectory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(caseDirectory, 'example.tsx'), 'utf8') expect(source).toContain('radialBarAngle') expect(source).toContain("angle: 'frequency'") diff --git a/benchmarks/conformance/cases/100-radial-bars/tanstack.ts b/benchmarks/conformance/cases/100-radial-bars/tanstack.ts index 8bdcbe87..6c029e65 100644 --- a/benchmarks/conformance/cases/100-radial-bars/tanstack.ts +++ b/benchmarks/conformance/cases/100-radial-bars/tanstack.ts @@ -1,54 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { polar, radialBarAngle } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { scaleBand, scaleLinear } from 'd3-scale' -import { selectRadialBarData } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const innerRadiusRatio = 0.2 -const colors = ['#7c3aed', '#0ea5e9', '#14b8a6', '#f59e0b'] -const maximumFrequency = alphabet[0]?.frequency ?? 1 +export * from './example' -export const radialBarsDefinition = (input: ConformanceInput) => { - const data = selectRadialBarData(alphabet, input.revision) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.84, - angle: { - scale: scaleLinear().domain([0, maximumFrequency]), - }, - radius: { - scale: () => - scaleBand().paddingInner(0.38).paddingOuter(0.19), - range: [ - ({ radius }) => radius * innerRadiusRatio, - ({ radius }) => radius, - ], - }, - marks: [ - radialBarAngle(data, { - id: 'letter-bars', - className: 'ts-chart__radial-bars', - angle: 'frequency', - radius: 'letter', - key: 'letter', - color: 'letter', - cornerRadius: 'full', - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const catalogCase = tanstackCase( - radialBarsDefinition, - 'Concentric letter frequency bars', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/101-sunburst/example.tsx b/benchmarks/conformance/cases/101-sunburst/example.tsx new file mode 100644 index 00000000..2efb002d --- /dev/null +++ b/benchmarks/conformance/cases/101-sunburst/example.tsx @@ -0,0 +1,74 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { sunburst } from '@tanstack/charts/hierarchy/sunburst' +import { polar } from '@tanstack/charts/polar' +import { flare } from '@charts-poc/demo-data/flare' +import { selectSunburstData } from './selection' + +export const sunburstDefinition = (input: ExampleOptions) => { + const data = selectSunburstData(flare, input.revision) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.88, + startAngle: Math.PI / 2, + endAngle: Math.PI / 2 - Math.PI * 2, + marks: [ + sunburst(data, { + id: 'sunburst-arcs', + path: 'name', + delimiter: '.', + value: 'size', + innerRadius: ({ radius }) => radius * 0.14, + outerRadius: ({ radius }) => { + const innerRadius = radius * 0.14 + return innerRadius + ((radius - innerRadius) * 2) / 3 + 2 + }, + ringPadding: 2, + color: 'branchId', + stroke: '#ffffff', + strokeWidth: 2, + }), + ], + }), + ], + color: { + range: ['#7c3aed', '#0ea5e9', '#14b8a6'], + }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Flare analytics sunburst' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(sunburstDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${(datum.data?.name ?? datum.id).replaceAll('.', ' › ')} · ${datum.value.toLocaleString('en-US')}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/101-sunburst/tanstack.test.ts b/benchmarks/conformance/cases/101-sunburst/tanstack.test.ts index be082c19..a18f85aa 100644 --- a/benchmarks/conformance/cases/101-sunburst/tanstack.test.ts +++ b/benchmarks/conformance/cases/101-sunburst/tanstack.test.ts @@ -172,7 +172,7 @@ describe('native responsive sunburst', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/101-sunburst/tanstack.ts', + 'benchmarks/conformance/cases/101-sunburst/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/101-sunburst/tanstack.ts b/benchmarks/conformance/cases/101-sunburst/tanstack.ts index 7b17a472..6c029e65 100644 --- a/benchmarks/conformance/cases/101-sunburst/tanstack.ts +++ b/benchmarks/conformance/cases/101-sunburst/tanstack.ts @@ -1,53 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { sunburst } from '@tanstack/charts/hierarchy/sunburst' -import { polar } from '@tanstack/charts/polar' -import { flare } from '@charts-poc/demo-data/flare' -import { selectSunburstData } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const sunburstDefinition = (input: ConformanceInput) => { - const data = selectSunburstData(flare, input.revision) +export * from './example' - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.88, - startAngle: Math.PI / 2, - endAngle: Math.PI / 2 - Math.PI * 2, - marks: [ - sunburst(data, { - id: 'sunburst-arcs', - path: 'name', - delimiter: '.', - value: 'size', - innerRadius: ({ radius }) => radius * 0.14, - outerRadius: ({ radius }) => { - const innerRadius = radius * 0.14 - return innerRadius + ((radius - innerRadius) * 2) / 3 + 2 - }, - ringPadding: 2, - color: 'branchId', - stroke: '#ffffff', - strokeWidth: 2, - }), - ], - }), - ], - color: { - range: ['#7c3aed', '#0ea5e9', '#14b8a6'], - }, - margin: 0, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const catalogCase = tanstackCase( - sunburstDefinition, - 'Flare analytics sunburst', - { - format: ({ datum }) => - `${(datum.data?.name ?? datum.id).replaceAll('.', ' › ')} · ${datum.value.toLocaleString('en-US')}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/102-world-choropleth/example.tsx b/benchmarks/conformance/cases/102-world-choropleth/example.tsx new file mode 100644 index 00000000..43217da4 --- /dev/null +++ b/benchmarks/conformance/cases/102-world-choropleth/example.tsx @@ -0,0 +1,99 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoEqualEarth } from 'd3-geo' +import { scaleThreshold } from 'd3-scale' +import { + previewWorldLand, + worldLand, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' +import { + learningPovertyCountries, + previewLearningPovertyCountries, +} from '@charts-poc/demo-data/learning-poverty-geography' + +const colorRanges = [ + ['#eff6ff', '#bfdbfe', '#60a5fa', '#2563eb', '#1e3a8a'], + ['#ecfeff', '#a5f3fc', '#22d3ee', '#0891b2', '#164e63'], +] +const thresholds = [20, 40, 60, 80] +const projection = { + type: geoEqualEarth, + fit: 'sphere' as const, +} +const previewProjection = { + type: () => geoEqualEarth().precision(2), + fit: 'sphere' as const, +} + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape([input.preview ? previewWorldLand : worldLand], { + projection: input.preview ? previewProjection : projection, + fill: '#e2e8f0', + stroke: '#ffffff', + strokeWidth: 0.5, + }), + geoShape( + input.preview + ? previewLearningPovertyCountries + : learningPovertyCountries, + { + projection: input.preview ? previewProjection : projection, + color: (country) => country.properties['Learning Poverty'], + stroke: '#ffffff', + strokeWidth: 0.5, + }, + ), + geoShape([worldSphere], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.35, + strokeWidth: 0.75, + }), + ], + color: { + scale: scaleThreshold, + domain: thresholds, + range: colorRanges[input.revision % 2] ?? colorRanges[0], + }, + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'World learning-poverty choropleth' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'properties' in datum && 'Learning Poverty' in datum.properties + ? `${datum.properties['Country Name']} · ${datum.properties['Learning Poverty']}% learning poverty` + : 'World land', + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/102-world-choropleth/plot.ts b/benchmarks/conformance/cases/102-world-choropleth/plot.ts index 80fcad60..f33d334f 100644 --- a/benchmarks/conformance/cases/102-world-choropleth/plot.ts +++ b/benchmarks/conformance/cases/102-world-choropleth/plot.ts @@ -1,8 +1,8 @@ import * as Plot from '@observablehq/plot' -import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas' -import { learningPovertyCountries } from '../../shared/transforms/learning-poverty' +import { worldLand, worldSphere } from '@charts-poc/demo-data/country-atlas' +import { learningPovertyCountries } from '@charts-poc/demo-data/learning-poverty-geography' import { mountObservablePlot } from '../../shared/mount' -import type { LearningPovertyCountry } from '../../shared/transforms/learning-poverty' +import type { LearningPovertyCountry } from '@charts-poc/demo-data/learning-poverty-geography' import type { ConformanceMount } from '../../types' const colorRanges = [ diff --git a/benchmarks/conformance/cases/102-world-choropleth/tanstack.ts b/benchmarks/conformance/cases/102-world-choropleth/tanstack.ts index 0e892790..6c029e65 100644 --- a/benchmarks/conformance/cases/102-world-choropleth/tanstack.ts +++ b/benchmarks/conformance/cases/102-world-choropleth/tanstack.ts @@ -1,76 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoEqualEarth } from 'd3-geo' -import { scaleThreshold } from 'd3-scale' -import { - previewWorldLand, - worldLand, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { - learningPovertyCountries, - previewLearningPovertyCountries, -} from '../../shared/transforms/learning-poverty' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colorRanges = [ - ['#eff6ff', '#bfdbfe', '#60a5fa', '#2563eb', '#1e3a8a'], - ['#ecfeff', '#a5f3fc', '#22d3ee', '#0891b2', '#164e63'], -] -const thresholds = [20, 40, 60, 80] -const projection = { - type: geoEqualEarth, - fit: 'sphere' as const, -} -const previewProjection = { - type: () => geoEqualEarth().precision(2), - fit: 'sphere' as const, -} +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape([input.preview ? previewWorldLand : worldLand], { - projection: input.preview ? previewProjection : projection, - fill: '#e2e8f0', - stroke: '#ffffff', - strokeWidth: 0.5, - }), - geoShape( - input.preview - ? previewLearningPovertyCountries - : learningPovertyCountries, - { - projection: input.preview ? previewProjection : projection, - color: (country) => country.properties['Learning Poverty'], - stroke: '#ffffff', - strokeWidth: 0.5, - }, - ), - geoShape([worldSphere], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.35, - strokeWidth: 0.75, - }), - ], - color: { - scale: scaleThreshold, - domain: thresholds, - range: colorRanges[input.revision % 2] ?? colorRanges[0], - }, - margin: 10, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'World learning-poverty choropleth', - { - format: ({ datum }) => - 'properties' in datum && 'Learning Poverty' in datum.properties - ? `${datum.properties['Country Name']} · ${datum.properties['Learning Poverty']}% learning poverty` - : 'World land', - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/103-bubble-map/example.tsx b/benchmarks/conformance/cases/103-bubble-map/example.tsx new file mode 100644 index 00000000..7ca132f3 --- /dev/null +++ b/benchmarks/conformance/cases/103-bubble-map/example.tsx @@ -0,0 +1,87 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoEqualEarth } from 'd3-geo' +import { scaleSqrt } from 'd3-scale' +import { + previewWorldLand, + worldLand, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' +import { learningPovertyPointsByPopulation } from '@charts-poc/demo-data/learning-poverty-geography' + +const fills = ['#2563eb', '#0891b2'] +const projection = { + type: geoEqualEarth, + fit: 'sphere' as const, +} +const previewProjection = { + type: () => geoEqualEarth().precision(2), + fit: 'sphere' as const, +} + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape([input.preview ? previewWorldLand : worldLand], { + projection: input.preview ? previewProjection : projection, + fill: '#e2e8f0', + stroke: '#ffffff', + strokeWidth: 0.5, + }), + geoShape(learningPovertyPointsByPopulation, { + projection: input.preview ? previewProjection : projection, + r: (country) => country.properties.population, + rScale: { + scale: () => scaleSqrt().range([2, 18]), + }, + fill: fills[input.revision % 2] ?? fills[0], + fillOpacity: 0.72, + stroke: '#ffffff', + strokeWidth: 0.75, + }), + geoShape([worldSphere], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.35, + strokeWidth: 0.75, + }), + ], + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'World population bubble map' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'properties' in datum && 'population' in datum.properties + ? `${datum.properties['Country Name']} · ${datum.properties.population.toLocaleString()} people` + : 'World land', + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/103-bubble-map/plot.ts b/benchmarks/conformance/cases/103-bubble-map/plot.ts index b2ef3abb..33b6f087 100644 --- a/benchmarks/conformance/cases/103-bubble-map/plot.ts +++ b/benchmarks/conformance/cases/103-bubble-map/plot.ts @@ -1,8 +1,8 @@ import * as Plot from '@observablehq/plot' -import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas' -import { learningPovertyPointsByPopulation } from '../../shared/transforms/learning-poverty' +import { worldLand, worldSphere } from '@charts-poc/demo-data/country-atlas' +import { learningPovertyPointsByPopulation } from '@charts-poc/demo-data/learning-poverty-geography' import { mountObservablePlot } from '../../shared/mount' -import type { LearningPovertyPoint } from '../../shared/transforms/learning-poverty' +import type { LearningPovertyPoint } from '@charts-poc/demo-data/learning-poverty-geography' import type { ConformanceMount } from '../../types' const fills = ['#2563eb', '#0891b2'] diff --git a/benchmarks/conformance/cases/103-bubble-map/tanstack.ts b/benchmarks/conformance/cases/103-bubble-map/tanstack.ts index e86added..6c029e65 100644 --- a/benchmarks/conformance/cases/103-bubble-map/tanstack.ts +++ b/benchmarks/conformance/cases/103-bubble-map/tanstack.ts @@ -1,60 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoEqualEarth } from 'd3-geo' -import { scaleSqrt } from 'd3-scale' -import { - previewWorldLand, - worldLand, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { learningPovertyPointsByPopulation } from '../../shared/transforms/learning-poverty' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const fills = ['#2563eb', '#0891b2'] -const projection = { - type: geoEqualEarth, - fit: 'sphere' as const, -} -const previewProjection = { - type: () => geoEqualEarth().precision(2), - fit: 'sphere' as const, -} +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape([input.preview ? previewWorldLand : worldLand], { - projection: input.preview ? previewProjection : projection, - fill: '#e2e8f0', - stroke: '#ffffff', - strokeWidth: 0.5, - }), - geoShape(learningPovertyPointsByPopulation, { - projection: input.preview ? previewProjection : projection, - r: (country) => country.properties.population, - rScale: { - scale: () => scaleSqrt().range([2, 18]), - }, - fill: fills[input.revision % 2] ?? fills[0], - fillOpacity: 0.72, - stroke: '#ffffff', - strokeWidth: 0.75, - }), - geoShape([worldSphere], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.35, - strokeWidth: 0.75, - }), - ], - margin: 10, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount(definition, 'World population bubble map', { - format: ({ datum }) => - 'properties' in datum && 'population' in datum.properties - ? `${datum.properties['Country Name']} · ${datum.properties.population.toLocaleString()} people` - : 'World land', -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/104-orthographic-globe/example.tsx b/benchmarks/conformance/cases/104-orthographic-globe/example.tsx new file mode 100644 index 00000000..415ac280 --- /dev/null +++ b/benchmarks/conformance/cases/104-orthographic-globe/example.tsx @@ -0,0 +1,80 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoOrthographic } from 'd3-geo' +import { + worldGraticule, + previewWorldGraticule, + previewWorldLand, + worldLand, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape([worldSphere], { + projection: { + type: () => geoOrthographic().rotate([0, -30, 20]), + fit: 'sphere', + }, + fill: '#dbeafe', + stroke: '#64748b', + strokeWidth: 1.25, + }), + geoShape([input.preview ? previewWorldGraticule : worldGraticule], { + projection: { + type: () => + geoOrthographic() + .rotate([0, -30, 20]) + .precision(input.preview ? 2 : Math.SQRT1_2), + fit: 'sphere', + }, + fill: 'none', + stroke: '#94a3b8', + strokeOpacity: 0.5, + strokeWidth: 0.75, + }), + geoShape([input.preview ? previewWorldLand : worldLand], { + projection: { + type: () => + geoOrthographic() + .rotate([0, -30, 20]) + .precision(input.preview ? 2 : Math.SQRT1_2), + fit: 'sphere', + }, + fill: input.revision % 2 === 0 ? '#22c55e' : '#0d9488', + fillOpacity: 0.82, + stroke: '#f8fafc', + strokeWidth: 0.75, + }), + ], + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Orthographic globe with graticule' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/104-orthographic-globe/plot.ts b/benchmarks/conformance/cases/104-orthographic-globe/plot.ts index 6859c701..228d9c66 100644 --- a/benchmarks/conformance/cases/104-orthographic-globe/plot.ts +++ b/benchmarks/conformance/cases/104-orthographic-globe/plot.ts @@ -3,7 +3,7 @@ import { worldGraticule, worldLand, worldSphere, -} from '../../shared/fixtures/country-atlas' +} from '@charts-poc/demo-data/country-atlas' import { mountObservablePlot } from '../../shared/mount' import type { ConformanceMount } from '../../types' diff --git a/benchmarks/conformance/cases/104-orthographic-globe/tanstack.ts b/benchmarks/conformance/cases/104-orthographic-globe/tanstack.ts index d12f24fd..6c029e65 100644 --- a/benchmarks/conformance/cases/104-orthographic-globe/tanstack.ts +++ b/benchmarks/conformance/cases/104-orthographic-globe/tanstack.ts @@ -1,59 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoOrthographic } from 'd3-geo' -import { - worldGraticule, - previewWorldGraticule, - previewWorldLand, - worldLand, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape([worldSphere], { - projection: { - type: () => geoOrthographic().rotate([0, -30, 20]), - fit: 'sphere', - }, - fill: '#dbeafe', - stroke: '#64748b', - strokeWidth: 1.25, - }), - geoShape([input.preview ? previewWorldGraticule : worldGraticule], { - projection: { - type: () => - geoOrthographic() - .rotate([0, -30, 20]) - .precision(input.preview ? 2 : Math.SQRT1_2), - fit: 'sphere', - }, - fill: 'none', - stroke: '#94a3b8', - strokeOpacity: 0.5, - strokeWidth: 0.75, - }), - geoShape([input.preview ? previewWorldLand : worldLand], { - projection: { - type: () => - geoOrthographic() - .rotate([0, -30, 20]) - .precision(input.preview ? 2 : Math.SQRT1_2), - fit: 'sphere', - }, - fill: input.revision % 2 === 0 ? '#22c55e' : '#0d9488', - fillOpacity: 0.82, - stroke: '#f8fafc', - strokeWidth: 0.75, - }), - ], - margin: 10, - }) +export * from './example' -export const mount = tanstackMount( - definition, - 'Orthographic globe with graticule', -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/105-route-map/example.tsx b/benchmarks/conformance/cases/105-route-map/example.tsx new file mode 100644 index 00000000..dc314bc6 --- /dev/null +++ b/benchmarks/conformance/cases/105-route-map/example.tsx @@ -0,0 +1,83 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoEqualEarth } from 'd3-geo' +import { + detailedWorldLand, + previewWorldGraticule, + previewWorldLand, + worldGraticule, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' +import { beagleRoute } from './transform' + +const routeColors = ['#dc2626', '#2563eb'] +const projection = { + type: () => geoEqualEarth().rotate([-10, 0]), + fit: 'sphere' as const, +} +const previewProjection = { + type: () => geoEqualEarth().rotate([-10, 0]).precision(2), + fit: 'sphere' as const, +} + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape([input.preview ? previewWorldLand : detailedWorldLand], { + projection: input.preview ? previewProjection : projection, + fill: '#e2e8f0', + stroke: '#ffffff', + strokeWidth: 0.5, + }), + geoShape([input.preview ? previewWorldGraticule : worldGraticule], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.2, + strokeWidth: 0.5, + }), + geoShape([beagleRoute], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: routeColors[input.revision % 2] ?? routeColors[0], + strokeWidth: 2, + strokeOpacity: 0.9, + }), + geoShape([worldSphere], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.4, + strokeWidth: 0.75, + }), + ], + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'HMS Beagle voyage' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/105-route-map/plot.ts b/benchmarks/conformance/cases/105-route-map/plot.ts index 7aa90950..b3781c1d 100644 --- a/benchmarks/conformance/cases/105-route-map/plot.ts +++ b/benchmarks/conformance/cases/105-route-map/plot.ts @@ -3,7 +3,7 @@ import { detailedWorldLand, worldGraticule, worldSphere, -} from '../../shared/fixtures/country-atlas' +} from '@charts-poc/demo-data/country-atlas' import { beagleRoute } from './transform' import { mountObservablePlot } from '../../shared/mount' import type { ConformanceMount } from '../../types' diff --git a/benchmarks/conformance/cases/105-route-map/tanstack.ts b/benchmarks/conformance/cases/105-route-map/tanstack.ts index d8ba8113..6c029e65 100644 --- a/benchmarks/conformance/cases/105-route-map/tanstack.ts +++ b/benchmarks/conformance/cases/105-route-map/tanstack.ts @@ -1,59 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoEqualEarth } from 'd3-geo' -import { - detailedWorldLand, - previewWorldGraticule, - previewWorldLand, - worldGraticule, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { beagleRoute } from './transform' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const routeColors = ['#dc2626', '#2563eb'] -const projection = { - type: () => geoEqualEarth().rotate([-10, 0]), - fit: 'sphere' as const, -} -const previewProjection = { - type: () => geoEqualEarth().rotate([-10, 0]).precision(2), - fit: 'sphere' as const, -} +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape([input.preview ? previewWorldLand : detailedWorldLand], { - projection: input.preview ? previewProjection : projection, - fill: '#e2e8f0', - stroke: '#ffffff', - strokeWidth: 0.5, - }), - geoShape([input.preview ? previewWorldGraticule : worldGraticule], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.2, - strokeWidth: 0.5, - }), - geoShape([beagleRoute], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: routeColors[input.revision % 2] ?? routeColors[0], - strokeWidth: 2, - strokeOpacity: 0.9, - }), - geoShape([worldSphere], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.4, - strokeWidth: 0.75, - }), - ], - margin: 10, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount(definition, 'HMS Beagle voyage') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/106-polar-line/example.tsx b/benchmarks/conformance/cases/106-polar-line/example.tsx new file mode 100644 index 00000000..926e6806 --- /dev/null +++ b/benchmarks/conformance/cases/106-polar-line/example.tsx @@ -0,0 +1,87 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { + angleGrid, + polar, + radialGrid, + radialLine, +} from '@tanstack/charts/polar' +import { scaleLinear } from 'd3-scale' +import { dayOfYearAngle, seattleWeatherYear } from './transform' + +const angleDomain = [0, 360] as const +const radiusDomain = [-10, 40] as const +const angleGridValues = [0, 45, 90, 135, 180, 225, 270, 315] as const +const radiusGridValues = [0, 10, 20, 30, 40] as const +const lineColor = '#0f766e' +const gridColor = '#94a3b8' + +export const definition = (input: ExampleOptions) => { + const rows = seattleWeatherYear(input.revision) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.72, + angle: { scale: scaleLinear().domain(angleDomain) }, + radius: { scale: scaleLinear().domain(radiusDomain) }, + guides: [ + radialGrid({ + values: radiusGridValues, + labels: false, + stroke: gridColor, + strokeOpacity: 0.35, + }), + angleGrid({ + values: angleGridValues, + labels: false, + stroke: gridColor, + strokeOpacity: 0.35, + }), + ], + marks: [ + radialLine(rows, { + angle: dayOfYearAngle, + radius: 'temp_max', + stroke: lineColor, + strokeWidth: 2.5, + }), + ], + }), + ], + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Seattle daily high-temperature polar line' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.date.toISOString().slice(0, 10)} · ${datum.temp_max}°C high`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/106-polar-line/tanstack.ts b/benchmarks/conformance/cases/106-polar-line/tanstack.ts index a656517d..6c029e65 100644 --- a/benchmarks/conformance/cases/106-polar-line/tanstack.ts +++ b/benchmarks/conformance/cases/106-polar-line/tanstack.ts @@ -1,70 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { - angleGrid, - polar, - radialGrid, - radialLine, -} from '@tanstack/charts/polar' -import { scaleLinear } from 'd3-scale' -import { dayOfYearAngle, seattleWeatherYear } from './transform' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const angleDomain = [0, 360] as const -const radiusDomain = [-10, 40] as const -const angleGridValues = [0, 45, 90, 135, 180, 225, 270, 315] as const -const radiusGridValues = [0, 10, 20, 30, 40] as const -const lineColor = '#0f766e' -const gridColor = '#94a3b8' +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = samplePreviewData( - seattleWeatherYear(input.revision), - input, - 80, - [(row) => dayOfYearAngle(row), (row) => row.temp_max], - ) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.72, - angle: { scale: scaleLinear().domain(angleDomain) }, - radius: { scale: scaleLinear().domain(radiusDomain) }, - guides: [ - radialGrid({ - values: radiusGridValues, - labels: false, - stroke: gridColor, - strokeOpacity: 0.35, - }), - angleGrid({ - values: angleGridValues, - labels: false, - stroke: gridColor, - strokeOpacity: 0.35, - }), - ], - marks: [ - radialLine(rows, { - angle: dayOfYearAngle, - radius: 'temp_max', - stroke: lineColor, - strokeWidth: 2.5, - }), - ], - }), - ], - margin: 0, - }) -} - -export const mount = tanstackMount( - definition, - 'Seattle daily high-temperature polar line', - { - format: ({ datum }) => - `${datum.date.toISOString().slice(0, 10)} · ${datum.temp_max}°C high`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/107-polar-scatter/example.tsx b/benchmarks/conformance/cases/107-polar-scatter/example.tsx new file mode 100644 index 00000000..323f1c7f --- /dev/null +++ b/benchmarks/conformance/cases/107-polar-scatter/example.tsx @@ -0,0 +1,84 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { angleGrid, polar, radialDot, radialGrid } from '@tanstack/charts/polar' +import { scaleLinear } from 'd3-scale' +import { windDirection, windLatitudeBand, windSpeed } from './transform' + +const angleDomain = [0, 360] as const +const radiusDomain = [0, 13] as const +const angleGridValues = [0, 45, 90, 135, 180, 225, 270, 315] as const +const radiusGridValues = [3, 6, 9, 12] as const +const dotColor = '#e11d48' +const gridColor = '#94a3b8' + +export const definition = (input: ExampleOptions) => { + const rows = windLatitudeBand(input.revision) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.72, + angle: { scale: scaleLinear().domain(angleDomain) }, + radius: { scale: scaleLinear().domain(radiusDomain) }, + guides: [ + radialGrid({ + values: radiusGridValues, + labels: false, + stroke: gridColor, + strokeOpacity: 0.35, + }), + angleGrid({ + values: angleGridValues, + labels: false, + stroke: gridColor, + strokeOpacity: 0.35, + }), + ], + marks: [ + radialDot(rows, { + angle: windDirection, + radius: windSpeed, + r: 4.5, + fill: dotColor, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + }), + ], + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Surface wind polar scatter' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.latitude}°N, ${datum.longitude}°E · ${windSpeed(datum).toFixed(1)} m/s`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/107-polar-scatter/tanstack.ts b/benchmarks/conformance/cases/107-polar-scatter/tanstack.ts index 9a352a5e..6c029e65 100644 --- a/benchmarks/conformance/cases/107-polar-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/107-polar-scatter/tanstack.ts @@ -1,57 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { angleGrid, polar, radialDot, radialGrid } from '@tanstack/charts/polar' -import { scaleLinear } from 'd3-scale' -import { windDirection, windLatitudeBand, windSpeed } from './transform' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const angleDomain = [0, 360] as const -const radiusDomain = [0, 13] as const -const angleGridValues = [0, 45, 90, 135, 180, 225, 270, 315] as const -const radiusGridValues = [3, 6, 9, 12] as const -const dotColor = '#e11d48' -const gridColor = '#94a3b8' +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = windLatitudeBand(input.revision) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.72, - angle: { scale: scaleLinear().domain(angleDomain) }, - radius: { scale: scaleLinear().domain(radiusDomain) }, - guides: [ - radialGrid({ - values: radiusGridValues, - labels: false, - stroke: gridColor, - strokeOpacity: 0.35, - }), - angleGrid({ - values: angleGridValues, - labels: false, - stroke: gridColor, - strokeOpacity: 0.35, - }), - ], - marks: [ - radialDot(rows, { - angle: windDirection, - radius: windSpeed, - r: 4.5, - fill: dotColor, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - }), - ], - margin: 0, - }) -} - -export const mount = tanstackMount(definition, 'Surface wind polar scatter', { - format: ({ datum }) => - `${datum.latitude}°N, ${datum.longitude}°E · ${windSpeed(datum).toFixed(1)} m/s`, -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/108-country-choropleth/example.tsx b/benchmarks/conformance/cases/108-country-choropleth/example.tsx new file mode 100644 index 00000000..c518ff0e --- /dev/null +++ b/benchmarks/conformance/cases/108-country-choropleth/example.tsx @@ -0,0 +1,98 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoEqualEarth } from 'd3-geo' +import { scaleQuantize } from 'd3-scale' +import { + previewWorldLand, + worldLand, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' +import { + learningPovertyCountries, + previewLearningPovertyCountries, +} from '@charts-poc/demo-data/learning-poverty-geography' + +const colorRanges = [ + ['#ecfeff', '#a5f3fc', '#67e8f9', '#06b6d4', '#0e7490', '#164e63'], + ['#f0fdf4', '#bbf7d0', '#86efac', '#22c55e', '#15803d', '#14532d'], +] +const projection = { + type: geoEqualEarth, + fit: 'sphere' as const, +} +const previewProjection = { + type: () => geoEqualEarth().precision(2), + fit: 'sphere' as const, +} + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape([input.preview ? previewWorldLand : worldLand], { + projection: input.preview ? previewProjection : projection, + fill: '#e2e8f0', + stroke: '#ffffff', + strokeWidth: 0.55, + }), + geoShape( + input.preview + ? previewLearningPovertyCountries + : learningPovertyCountries, + { + projection: input.preview ? previewProjection : projection, + color: (country) => country.properties.density, + stroke: 'currentColor', + strokeOpacity: 0.34, + strokeWidth: 0.55, + }, + ), + geoShape([worldSphere], { + projection: input.preview ? previewProjection : projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.35, + strokeWidth: 0.75, + }), + ], + color: { + scale: scaleQuantize, + range: colorRanges[input.revision % 2] ?? colorRanges[0], + }, + margin: 12, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'World population-density choropleth' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'properties' in datum && 'density' in datum.properties + ? `${datum.properties['Country Name']} · ${datum.properties.density} people/km²` + : 'World land', + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/108-country-choropleth/plot.ts b/benchmarks/conformance/cases/108-country-choropleth/plot.ts index cc066fc0..37731f52 100644 --- a/benchmarks/conformance/cases/108-country-choropleth/plot.ts +++ b/benchmarks/conformance/cases/108-country-choropleth/plot.ts @@ -1,8 +1,8 @@ import * as Plot from '@observablehq/plot' -import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas' -import { learningPovertyCountries } from '../../shared/transforms/learning-poverty' +import { worldLand, worldSphere } from '@charts-poc/demo-data/country-atlas' +import { learningPovertyCountries } from '@charts-poc/demo-data/learning-poverty-geography' import { mountObservablePlot } from '../../shared/mount' -import type { LearningPovertyCountry } from '../../shared/transforms/learning-poverty' +import type { LearningPovertyCountry } from '@charts-poc/demo-data/learning-poverty-geography' import type { ConformanceMount } from '../../types' const colorRanges = [ diff --git a/benchmarks/conformance/cases/108-country-choropleth/tanstack.ts b/benchmarks/conformance/cases/108-country-choropleth/tanstack.ts index da9d95a8..6c029e65 100644 --- a/benchmarks/conformance/cases/108-country-choropleth/tanstack.ts +++ b/benchmarks/conformance/cases/108-country-choropleth/tanstack.ts @@ -1,75 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoEqualEarth } from 'd3-geo' -import { scaleQuantize } from 'd3-scale' -import { - previewWorldLand, - worldLand, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { - learningPovertyCountries, - previewLearningPovertyCountries, -} from '../../shared/transforms/learning-poverty' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colorRanges = [ - ['#ecfeff', '#a5f3fc', '#67e8f9', '#06b6d4', '#0e7490', '#164e63'], - ['#f0fdf4', '#bbf7d0', '#86efac', '#22c55e', '#15803d', '#14532d'], -] -const projection = { - type: geoEqualEarth, - fit: 'sphere' as const, -} -const previewProjection = { - type: () => geoEqualEarth().precision(2), - fit: 'sphere' as const, -} +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape([input.preview ? previewWorldLand : worldLand], { - projection: input.preview ? previewProjection : projection, - fill: '#e2e8f0', - stroke: '#ffffff', - strokeWidth: 0.55, - }), - geoShape( - input.preview - ? previewLearningPovertyCountries - : learningPovertyCountries, - { - projection: input.preview ? previewProjection : projection, - color: (country) => country.properties.density, - stroke: 'currentColor', - strokeOpacity: 0.34, - strokeWidth: 0.55, - }, - ), - geoShape([worldSphere], { - projection: input.preview ? previewProjection : projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.35, - strokeWidth: 0.75, - }), - ], - color: { - scale: scaleQuantize, - range: colorRanges[input.revision % 2] ?? colorRanges[0], - }, - margin: 12, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'World population-density choropleth', - { - format: ({ datum }) => - 'properties' in datum && 'density' in datum.properties - ? `${datum.properties['Country Name']} · ${datum.properties.density} people/km²` - : 'World land', - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/109-us-state-choropleth/example.tsx b/benchmarks/conformance/cases/109-us-state-choropleth/example.tsx new file mode 100644 index 00000000..b155ed61 --- /dev/null +++ b/benchmarks/conformance/cases/109-us-state-choropleth/example.tsx @@ -0,0 +1,87 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoAlbersUsa } from 'd3-geo' +import { scaleQuantile } from 'd3-scale' +import { + projectedUnemploymentCounties, + unemploymentCountyCollection, +} from './transform' + +const colorRanges = [ + [ + '#f7fbff', + '#deebf7', + '#c6dbef', + '#9ecae1', + '#6baed6', + '#4292c6', + '#2171b5', + '#08519c', + '#08306b', + ], + [ + '#f7fcf5', + '#e5f5e0', + '#c7e9c0', + '#a1d99b', + '#74c476', + '#41ab5d', + '#238b45', + '#006d2c', + '#00441b', + ], +] + +export const usStateChoroplethDefinition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape(projectedUnemploymentCounties, { + projection: { + type: geoAlbersUsa, + fit: unemploymentCountyCollection, + }, + color: (county) => county.properties.rate, + stroke: '#f8fafc', + strokeWidth: 0.35, + }), + ], + color: { + scale: scaleQuantile, + range: colorRanges[input.revision % 2] ?? colorRanges[0], + }, + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'United States county unemployment choropleth' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(usStateChoroplethDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.properties.county}, ${datum.properties.state} · ${datum.properties.rate}% unemployment`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.test.ts b/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.test.ts index a7e7ccff..2213da48 100644 --- a/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.test.ts +++ b/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.test.ts @@ -78,7 +78,7 @@ describe('definition-owned Albers USA choropleth', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts', + 'benchmarks/conformance/cases/109-us-state-choropleth/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts b/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts index e00c504b..6c029e65 100644 --- a/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts +++ b/benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts @@ -1,102 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoAlbersUsa } from 'd3-geo' -import { scaleQuantile } from 'd3-scale' -import { - previewUnemploymentCounties, - previewUnemploymentCountyCollection, - projectedUnemploymentCounties, - unemploymentCountyCollection, -} from './transform' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colorRanges = [ - [ - '#f7fbff', - '#deebf7', - '#c6dbef', - '#9ecae1', - '#6baed6', - '#4292c6', - '#2171b5', - '#08519c', - '#08306b', - ], - [ - '#f7fcf5', - '#e5f5e0', - '#c7e9c0', - '#a1d99b', - '#74c476', - '#41ab5d', - '#238b45', - '#006d2c', - '#00441b', - ], -] +export * from './example' -export const usStateChoroplethDefinition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape(projectedUnemploymentCounties, { - projection: { - type: geoAlbersUsa, - fit: unemploymentCountyCollection, - }, - color: (county) => county.properties.rate, - stroke: '#f8fafc', - strokeWidth: 0.35, - }), - ], - color: { - scale: scaleQuantile, - range: colorRanges[input.revision % 2] ?? colorRanges[0], - }, - margin: 10, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const catalogChoroplethDefinition = (input: ConformanceInput) => { - const features = - input.preview === true - ? previewUnemploymentCounties - : projectedUnemploymentCounties - const fit = - input.preview === true - ? previewUnemploymentCountyCollection - : unemploymentCountyCollection - - return defineChart({ - marks: [ - geoShape(features, { - projection: { type: geoAlbersUsa, fit }, - color: (feature) => feature.properties.rate, - stroke: '#f8fafc', - strokeWidth: input.preview === true ? 0.25 : 0.35, - }), - ], - color: { - scale: scaleQuantile, - range: colorRanges[input.revision % 2] ?? colorRanges[0], - }, - margin: 10, - }) -} - -export const mount = tanstackMount( - usStateChoroplethDefinition, - 'United States county unemployment choropleth', - { - format: ({ datum }) => - `${datum.properties.county}, ${datum.properties.state} · ${datum.properties.rate}% unemployment`, - }, -) - -export const catalogCase = tanstackCase( - catalogChoroplethDefinition, - mount.ariaLabel, - { - format: ({ datum }) => - `${datum.properties.county}, ${datum.properties.state} · ${datum.properties.rate}% unemployment`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/109-us-state-choropleth/transform.ts b/benchmarks/conformance/cases/109-us-state-choropleth/transform.ts index 5ff825bc..b1a81e84 100644 --- a/benchmarks/conformance/cases/109-us-state-choropleth/transform.ts +++ b/benchmarks/conformance/cases/109-us-state-choropleth/transform.ts @@ -2,7 +2,7 @@ import { usCountyUnemployment } from '@charts-poc/demo-data/us-county-unemployme import countiesAtlasJson from 'us-atlas/counties-10m.json' import { geoAlbersUsa, geoPath } from 'd3-geo' import { feature } from 'topojson-client' -import { simplifyPolygonGeometry } from '../../shared/fixtures/simplify-geo' +import { simplifyPolygonGeometry } from '@charts-poc/demo-data/simplify-geo' import type { UsCountyUnemploymentRow } from '@charts-poc/demo-data/us-county-unemployment' import type { ExtendedFeature, diff --git a/benchmarks/conformance/cases/110-projection-gallery/example.tsx b/benchmarks/conformance/cases/110-projection-gallery/example.tsx new file mode 100644 index 00000000..b68ad3f6 --- /dev/null +++ b/benchmarks/conformance/cases/110-projection-gallery/example.tsx @@ -0,0 +1,92 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, facetChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { + previewWorldLand, + worldLand, + worldSphere, +} from '@charts-poc/demo-data/country-atlas' +import { projectionGalleryData } from './projection' + +const projectionColors = [ + ['#2563eb', '#7c3aed', '#0891b2', '#ea580c'], + ['#1d4ed8', '#6d28d9', '#0e7490', '#c2410c'], +] + +function projectionGalleryChart(input: ExampleOptions, preview: boolean) { + const projections = projectionGalleryData() + const color = { + domain: projections.map(({ id }) => id), + range: projectionColors[input.revision % 2] ?? projectionColors[0], + } + + return facetChart(projections, { + id: 'projection-gallery', + by: 'id', + columns: 2, + gap: 0, + label: false, + chart: ([entry]) => { + const projection = { + type: preview ? () => entry.create().precision(2) : entry.create, + fit: 'sphere' as const, + inset: 8, + } + + return { + marks: [ + geoShape([worldSphere], { + id: 'sphere', + projection, + fill: 'none', + stroke: 'currentColor', + strokeOpacity: 0.5, + strokeWidth: 0.8, + }), + geoShape([preview ? previewWorldLand : worldLand], { + id: 'land', + projection, + color: () => entry.id, + fillOpacity: 0.78, + stroke: 'currentColor', + strokeOpacity: 0.28, + strokeWidth: 0.45, + }), + ], + color, + guides: false, + margin: 0, + } + }, + }) +} + +export const projectionGalleryDefinition = (input: ExampleOptions) => + projectionGalleryChart(input, false) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Standard world projection gallery' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(projectionGalleryDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/110-projection-gallery/plot.ts b/benchmarks/conformance/cases/110-projection-gallery/plot.ts index f7661de4..9ee6a5ac 100644 --- a/benchmarks/conformance/cases/110-projection-gallery/plot.ts +++ b/benchmarks/conformance/cases/110-projection-gallery/plot.ts @@ -1,5 +1,5 @@ import * as Plot from '@observablehq/plot' -import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas' +import { worldLand, worldSphere } from '@charts-poc/demo-data/country-atlas' import { projectionGalleryData } from './projection' import type { ConformanceInput, ConformanceMount } from '../../types' import type { GeoProjection } from 'd3-geo' diff --git a/benchmarks/conformance/cases/110-projection-gallery/tanstack.test.ts b/benchmarks/conformance/cases/110-projection-gallery/tanstack.test.ts index 7bf09ea8..015f9b9d 100644 --- a/benchmarks/conformance/cases/110-projection-gallery/tanstack.test.ts +++ b/benchmarks/conformance/cases/110-projection-gallery/tanstack.test.ts @@ -3,10 +3,10 @@ import { resolve } from 'node:path' import { createChartRuntime } from '@tanstack/charts' import { geoPath } from 'd3-geo' import { describe, expect, expectTypeOf, it } from 'vitest' -import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas' +import { worldLand, worldSphere } from '@charts-poc/demo-data/country-atlas' import { projectionGalleryData } from './projection' import { projectionGalleryDefinition } from './tanstack' -import type { LandFeature } from '../../shared/fixtures/country-atlas' +import type { LandFeature } from '@charts-poc/demo-data/country-atlas' import type { ChartScene, ChartSpecDatum, @@ -106,7 +106,7 @@ describe('declarative projection gallery', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/110-projection-gallery/tanstack.ts', + 'benchmarks/conformance/cases/110-projection-gallery/example.tsx', ), 'utf8', ) @@ -118,7 +118,7 @@ describe('declarative projection gallery', () => { 'utf8', ) - expect(source).toContain("import { facetChart } from '@tanstack/charts'") + expect(source).toContain('facetChart(projections, {') expect(source).toContain("fit: 'sphere'") expect(source).toContain('inset: 8') expect(source).toContain('columns: 2') diff --git a/benchmarks/conformance/cases/110-projection-gallery/tanstack.ts b/benchmarks/conformance/cases/110-projection-gallery/tanstack.ts index 2c080251..6c029e65 100644 --- a/benchmarks/conformance/cases/110-projection-gallery/tanstack.ts +++ b/benchmarks/conformance/cases/110-projection-gallery/tanstack.ts @@ -1,80 +1,8 @@ -import { facetChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { - previewWorldLand, - worldLand, - worldSphere, -} from '../../shared/fixtures/country-atlas' -import { projectionGalleryData } from './projection' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const projectionColors = [ - ['#2563eb', '#7c3aed', '#0891b2', '#ea580c'], - ['#1d4ed8', '#6d28d9', '#0e7490', '#c2410c'], -] +export * from './example' -function projectionGalleryChart(input: ConformanceInput, preview: boolean) { - const projections = projectionGalleryData() - const color = { - domain: projections.map(({ id }) => id), - range: projectionColors[input.revision % 2] ?? projectionColors[0], - } +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return facetChart(projections, { - id: 'projection-gallery', - by: 'id', - columns: 2, - gap: 0, - label: false, - chart: ([entry]) => { - const projection = { - type: preview ? () => entry.create().precision(2) : entry.create, - fit: 'sphere' as const, - inset: 8, - } - - return { - marks: [ - geoShape([worldSphere], { - id: 'sphere', - projection, - fill: 'none', - stroke: 'currentColor', - strokeOpacity: 0.5, - strokeWidth: 0.8, - }), - geoShape([preview ? previewWorldLand : worldLand], { - id: 'land', - projection, - color: () => entry.id, - fillOpacity: 0.78, - stroke: 'currentColor', - strokeOpacity: 0.28, - strokeWidth: 0.45, - }), - ], - color, - guides: false, - margin: 0, - } - }, - }) -} - -export const projectionGalleryDefinition = (input: ConformanceInput) => - projectionGalleryChart(input, false) - -const catalogProjectionGalleryDefinition = (input: ConformanceInput) => - projectionGalleryChart(input, input.preview === true) - -export const mount = tanstackMount( - projectionGalleryDefinition, - 'Standard world projection gallery', -) - -export const catalogCase = tanstackCase( - catalogProjectionGalleryDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/111-basic-sankey/example.tsx b/benchmarks/conformance/cases/111-basic-sankey/example.tsx new file mode 100644 index 00000000..3cfebe6b --- /dev/null +++ b/benchmarks/conformance/cases/111-basic-sankey/example.tsx @@ -0,0 +1,145 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { d3Curve, defineChart, link, rect, text } from '@tanstack/charts' +import { sankeyDiagram } from '@tanstack/charts/network/sankey' +import { curveBumpX } from 'd3-shape' +import { basicSankeyData } from './model' +import type { SankeyLink, SankeyNode } from '@tanstack/charts/network/sankey' +import type { BasicFlowLink, BasicFlowNode } from './model' + +export type BasicSankeyNodeRow = SankeyNode< + BasicFlowNode, + BasicFlowLink, + string +> +export type BasicSankeyLinkRow = SankeyLink< + BasicFlowNode, + BasicFlowLink, + string +> +export type BasicSankeyDatum = BasicSankeyNodeRow | BasicSankeyLinkRow + +export const basicSankeyDefinition = (input: ExampleOptions) => { + const { nodes, links } = basicSankeyData(input.revision) + + return defineChart({ + marks: [ + sankeyDiagram({ + id: 'basic-sankey', + nodes, + links, + nodeKey: 'id', + source: 'source', + target: 'target', + value: 'value', + align: 'left', + nodeWidth: + input.preview === true + ? 8 + : ({ width }) => clamp(width * 0.025, 10, 18), + nodePadding: + input.preview === true + ? 8 + : ({ height }) => clamp(height * 0.12, 18, 38), + inset: + input.preview === true + ? 4 + : ({ width, height }) => ({ + left: clamp(width * 0.14, 48, 82), + right: clamp(width * 0.14, 48, 82), + top: clamp(height * 0.1, 18, 32), + bottom: clamp(height * 0.1, 18, 32), + }), + iterations: 16, + marks: ({ chart, nodes: sankeyNodes, links: sankeyLinks }) => { + const labelFontSize = clamp(chart.width * 0.018, 8, 12) + const labelOffset = clamp(chart.width * 0.012, 4, 8) + return [ + link(sankeyLinks, { + id: 'links', + x1: 'x1', + y1: 'y1', + x2: 'x2', + y2: 'y2', + key: 'key', + stroke: 'currentColor', + strokeOpacity: 0.35, + strokeWidth: (flow) => Math.max(1, flow.width), + lineCap: 'butt', + curve: d3Curve(curveBumpX), + }), + rect(sankeyNodes, { + id: 'nodes', + x1: 'x0', + x2: 'x1', + y1: 'y0', + y2: 'y1', + key: 'key', + fill: 'currentColor', + fillOpacity: 0.72, + inset: 0, + }), + ...(input.preview === true + ? [] + : [ + text(sankeyNodes, { + id: 'labels', + x: (node) => + node.depth === 0 + ? node.x0 - labelOffset + : node.x1 + labelOffset, + y: (node) => node.y, + text: (node) => node.data.label, + key: 'key', + anchor: (node) => (node.depth === 0 ? 'end' : 'start'), + fill: 'currentColor', + fontSize: labelFontSize, + fontWeight: 650, + }), + ]), + ] as const + }, + }), + ], + guides: false, + margin: 0, + }) +} + +function clamp(value: number, minimum: number, maximum: number) { + return Math.min(maximum, Math.max(minimum, value)) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Basic Sankey' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(basicSankeyDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + datum.kind === 'node' + ? `${datum.data.label} · ${datum.value}` + : `${datum.sourceNode.data.label} → ${datum.targetNode.data.label} · ${datum.value}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/111-basic-sankey/tanstack.test.ts b/benchmarks/conformance/cases/111-basic-sankey/tanstack.test.ts index 4978b67e..8a61b113 100644 --- a/benchmarks/conformance/cases/111-basic-sankey/tanstack.test.ts +++ b/benchmarks/conformance/cases/111-basic-sankey/tanstack.test.ts @@ -203,7 +203,7 @@ describe('basic Sankey composition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/111-basic-sankey/tanstack.ts', + 'benchmarks/conformance/cases/111-basic-sankey/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/111-basic-sankey/tanstack.ts b/benchmarks/conformance/cases/111-basic-sankey/tanstack.ts index 59c3f2bd..6c029e65 100644 --- a/benchmarks/conformance/cases/111-basic-sankey/tanstack.ts +++ b/benchmarks/conformance/cases/111-basic-sankey/tanstack.ts @@ -1,120 +1,8 @@ -import { d3Curve, defineChart, link, rect, text } from '@tanstack/charts' -import { sankeyDiagram } from '@tanstack/charts/network/sankey' -import { curveBumpX } from 'd3-shape' -import { basicSankeyData } from './model' -import { tanstackCase } from '../../shared/mount' -import type { SankeyLink, SankeyNode } from '@tanstack/charts/network/sankey' -import type { ConformanceInput } from '../../types' -import type { BasicFlowLink, BasicFlowNode } from './model' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export type BasicSankeyNodeRow = SankeyNode< - BasicFlowNode, - BasicFlowLink, - string -> -export type BasicSankeyLinkRow = SankeyLink< - BasicFlowNode, - BasicFlowLink, - string -> -export type BasicSankeyDatum = BasicSankeyNodeRow | BasicSankeyLinkRow +export * from './example' -export const basicSankeyDefinition = (input: ConformanceInput) => { - const { nodes, links } = basicSankeyData(input.revision) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - sankeyDiagram({ - id: 'basic-sankey', - nodes, - links, - nodeKey: 'id', - source: 'source', - target: 'target', - value: 'value', - align: 'left', - nodeWidth: - input.preview === true - ? 8 - : ({ width }) => clamp(width * 0.025, 10, 18), - nodePadding: - input.preview === true - ? 8 - : ({ height }) => clamp(height * 0.12, 18, 38), - inset: - input.preview === true - ? 4 - : ({ width, height }) => ({ - left: clamp(width * 0.14, 48, 82), - right: clamp(width * 0.14, 48, 82), - top: clamp(height * 0.1, 18, 32), - bottom: clamp(height * 0.1, 18, 32), - }), - iterations: 16, - marks: ({ chart, nodes: sankeyNodes, links: sankeyLinks }) => { - const labelFontSize = clamp(chart.width * 0.018, 8, 12) - const labelOffset = clamp(chart.width * 0.012, 4, 8) - return [ - link(sankeyLinks, { - id: 'links', - x1: 'x1', - y1: 'y1', - x2: 'x2', - y2: 'y2', - key: 'key', - stroke: 'currentColor', - strokeOpacity: 0.35, - strokeWidth: (flow) => Math.max(1, flow.width), - lineCap: 'butt', - curve: d3Curve(curveBumpX), - }), - rect(sankeyNodes, { - id: 'nodes', - x1: 'x0', - x2: 'x1', - y1: 'y0', - y2: 'y1', - key: 'key', - fill: 'currentColor', - fillOpacity: 0.72, - inset: 0, - }), - ...(input.preview === true - ? [] - : [ - text(sankeyNodes, { - id: 'labels', - x: (node) => - node.depth === 0 - ? node.x0 - labelOffset - : node.x1 + labelOffset, - y: (node) => node.y, - text: (node) => node.data.label, - key: 'key', - anchor: (node) => (node.depth === 0 ? 'end' : 'start'), - fill: 'currentColor', - fontSize: labelFontSize, - fontWeight: 650, - }), - ]), - ] as const - }, - }), - ], - guides: false, - margin: 0, - }) -} - -function clamp(value: number, minimum: number, maximum: number) { - return Math.min(maximum, Math.max(minimum, value)) -} - -export const catalogCase = tanstackCase(basicSankeyDefinition, 'Basic Sankey', { - format: ({ datum }) => - datum.kind === 'node' - ? `${datum.data.label} · ${datum.value}` - : `${datum.sourceNode.data.label} → ${datum.targetNode.data.label} · ${datum.value}`, -}) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/111-sankey-flow/example.tsx b/benchmarks/conformance/cases/111-sankey-flow/example.tsx new file mode 100644 index 00000000..6710b39b --- /dev/null +++ b/benchmarks/conformance/cases/111-sankey-flow/example.tsx @@ -0,0 +1,255 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { d3Curve, defineChart, link, rect, text } from '@tanstack/charts' +import { sankeyDiagram } from '@tanstack/charts/network/sankey' +import { curveBumpX } from 'd3-shape' +import { labelBackdropBounds } from './layout' +import { + incomeStatementData, + incomeStatementTitle, + linkColors, + toneColors, +} from './model' +import type { SankeyLink, SankeyNode } from '@tanstack/charts/network/sankey' +import type { FlowLink, FlowNode, FlowNodeId, FlowTone } from './model' + +const toneDomain = [ + 'Neutral', + 'Profit', + 'Cost', +] as const satisfies readonly FlowTone[] + +export type IncomeSankeyNodeRow = SankeyNode +export type IncomeSankeyLinkRow = SankeyLink + +export interface IncomeSankeyTitleRow { + readonly kind: 'title' + readonly id: 'title' + readonly title: string + readonly x: number + readonly y: number +} + +interface IncomeSankeyLabelRow extends IncomeSankeyNodeRow { + readonly labelText: string + readonly labelX: number + readonly labelNameY: number + readonly labelValueY: number + readonly labelAnchor: 'start' | 'end' + readonly backdropX0: number + readonly backdropX1: number + readonly backdropY0: number + readonly backdropY1: number +} + +export type IncomeSankeyDatum = + IncomeSankeyNodeRow | IncomeSankeyLinkRow | IncomeSankeyTitleRow + +export const sankeyDefinition = (input: ExampleOptions) => { + const sourceData = incomeStatementData(input.revision) + + return defineChart({ + marks: [ + sankeyDiagram({ + id: 'income-sankey', + nodes: sourceData.nodes, + links: sourceData.links, + nodeKey: 'id', + source: 'source', + target: 'target', + value: 'value', + align: 'left', + nodeSort: (left, right) => left.data.order - right.data.order, + nodeWidth: + input.preview === true + ? 8 + : ({ width }) => clamp(width * 0.032, 10, 24), + nodePadding: + input.preview === true + ? 3 + : ({ height }) => clamp(height * 0.11, 12, 40), + inset: + input.preview === true + ? 4 + : ({ width, height }) => ({ + left: clamp(width * 0.15, 56, 122), + right: clamp(width * 0.13, 48, 105), + top: clamp(height * 0.14, 38, 70), + bottom: clamp(height * 0.025, 8, 14), + }), + iterations: 32, + marks: ({ chart, nodes: sankeyNodes, links: sankeyLinks }) => { + const flowMarks = [ + link(sankeyLinks, { + id: 'links', + x1: 'x1', + y1: 'y1', + x2: 'x2', + y2: 'y2', + key: 'key', + stroke: (flow) => linkColors[flow.data.tone], + strokeOpacity: (flow) => + flow.data.tone === 'Neutral' ? 0.58 : 0.64, + strokeWidth: (flow) => Math.max(1, flow.width), + lineCap: 'butt', + curve: d3Curve(curveBumpX), + }), + rect(sankeyNodes, { + id: 'nodes', + x1: 'x0', + x2: 'x1', + y1: 'y0', + y2: 'y1', + key: 'key', + color: (node) => node.data.tone, + inset: 0, + }), + ] as const + if (input.preview === true) return flowMarks + + const labelFontSize = clamp(chart.width * 0.013, 6.5, 10.5) + const labelOffset = clamp(chart.width * 0.008, 3, 6) + const labelRows = sankeyNodes.map((node): IncomeSankeyLabelRow => { + const labelAnchor = + node.data.labelSide === 'right' ? 'start' : 'end' + const labelX = + node.data.labelSide === 'right' + ? node.x1 + labelOffset + : node.x0 - labelOffset + const labelText = + chart.width < 720 && node.data.compactLabel + ? node.data.compactLabel + : node.data.label + const backdrop = labelBackdropBounds({ + anchor: labelAnchor, + centerY: node.y, + fontSize: labelFontSize, + label: labelText, + labelX, + value: node.data.displayValue, + }) + return { + ...node, + labelText, + labelX, + labelNameY: node.y - labelFontSize * 0.5, + labelValueY: node.y + labelFontSize * 0.58, + labelAnchor, + backdropX0: backdrop.x, + backdropX1: backdrop.x + backdrop.width, + backdropY0: backdrop.y, + backdropY1: backdrop.y + backdrop.height, + } + }) + const backdropRows = labelRows.filter( + (node) => node.data.labelBackdrop, + ) + const titleRows: readonly IncomeSankeyTitleRow[] = [ + { + kind: 'title', + id: 'title', + title: incomeStatementTitle, + x: chart.x + chart.width / 2, + y: chart.y + clamp(chart.height * 0.065, 17, 32), + }, + ] + + return [ + ...flowMarks, + rect(backdropRows, { + id: 'label-backdrops', + x1: 'backdropX0', + x2: 'backdropX1', + y1: 'backdropY0', + y2: 'backdropY1', + key: 'key', + fill: 'var(--panel, #ffffff)', + fillOpacity: 0.82, + inset: 0, + radius: 1, + }), + text(labelRows, { + id: 'label-names', + x: 'labelX', + y: 'labelNameY', + text: 'labelText', + key: 'key', + anchor: (node) => node.labelAnchor, + fill: 'currentColor', + fontSize: labelFontSize, + fontWeight: 700, + }), + text(labelRows, { + id: 'label-values', + x: 'labelX', + y: 'labelValueY', + text: (node) => node.data.displayValue, + key: 'key', + anchor: (node) => node.labelAnchor, + fill: 'currentColor', + fontSize: labelFontSize, + fontWeight: 500, + }), + text(titleRows, { + id: 'title', + x: 'x', + y: 'y', + text: 'title', + key: 'id', + fill: '#155477', + fontSize: clamp(chart.width * 0.034, 14, 26), + fontWeight: 750, + }), + ] as const + }, + }), + ], + color: { + domain: toneDomain, + range: toneDomain.map((tone) => toneColors[tone]), + }, + guides: false, + margin: 0, + }) +} + +function clamp(value: number, minimum: number, maximum: number) { + return Math.min(maximum, Math.max(minimum, value)) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = incomeStatementTitle + +export const createExampleChart = (options: ExampleOptions) => + defineChart(sankeyDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => { + if (datum.kind === 'title') return datum.title + if (datum.kind === 'node') { + return `${datum.data.label} · ${datum.data.displayValue}` + } + return `${datum.sourceNode.data.label} → ${datum.targetNode.data.label} · ${datum.value}` + }, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/111-sankey-flow/tanstack.test.ts b/benchmarks/conformance/cases/111-sankey-flow/tanstack.test.ts index 8b749892..4ff704a2 100644 --- a/benchmarks/conformance/cases/111-sankey-flow/tanstack.test.ts +++ b/benchmarks/conformance/cases/111-sankey-flow/tanstack.test.ts @@ -277,7 +277,7 @@ describe('Apple income statement Sankey composition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/111-sankey-flow/tanstack.ts', + 'benchmarks/conformance/cases/111-sankey-flow/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/111-sankey-flow/tanstack.ts b/benchmarks/conformance/cases/111-sankey-flow/tanstack.ts index 1296c45c..6c029e65 100644 --- a/benchmarks/conformance/cases/111-sankey-flow/tanstack.ts +++ b/benchmarks/conformance/cases/111-sankey-flow/tanstack.ts @@ -1,228 +1,8 @@ -import { d3Curve, defineChart, link, rect, text } from '@tanstack/charts' -import { sankeyDiagram } from '@tanstack/charts/network/sankey' -import { curveBumpX } from 'd3-shape' -import { labelBackdropBounds } from './layout' -import { - incomeStatementData, - incomeStatementTitle, - linkColors, - toneColors, -} from './model' -import { tanstackMount } from '../../shared/mount' -import type { SankeyLink, SankeyNode } from '@tanstack/charts/network/sankey' -import type { FlowLink, FlowNode, FlowNodeId, FlowTone } from './model' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const toneDomain = [ - 'Neutral', - 'Profit', - 'Cost', -] as const satisfies readonly FlowTone[] +export * from './example' -export type IncomeSankeyNodeRow = SankeyNode -export type IncomeSankeyLinkRow = SankeyLink +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export interface IncomeSankeyTitleRow { - readonly kind: 'title' - readonly id: 'title' - readonly title: string - readonly x: number - readonly y: number -} - -interface IncomeSankeyLabelRow extends IncomeSankeyNodeRow { - readonly labelText: string - readonly labelX: number - readonly labelNameY: number - readonly labelValueY: number - readonly labelAnchor: 'start' | 'end' - readonly backdropX0: number - readonly backdropX1: number - readonly backdropY0: number - readonly backdropY1: number -} - -export type IncomeSankeyDatum = - IncomeSankeyNodeRow | IncomeSankeyLinkRow | IncomeSankeyTitleRow - -export const sankeyDefinition = (input: ConformanceInput) => { - const sourceData = incomeStatementData(input.revision) - - return defineChart({ - marks: [ - sankeyDiagram({ - id: 'income-sankey', - nodes: sourceData.nodes, - links: sourceData.links, - nodeKey: 'id', - source: 'source', - target: 'target', - value: 'value', - align: 'left', - nodeSort: (left, right) => left.data.order - right.data.order, - nodeWidth: - input.preview === true - ? 8 - : ({ width }) => clamp(width * 0.032, 10, 24), - nodePadding: - input.preview === true - ? 3 - : ({ height }) => clamp(height * 0.11, 12, 40), - inset: - input.preview === true - ? 4 - : ({ width, height }) => ({ - left: clamp(width * 0.15, 56, 122), - right: clamp(width * 0.13, 48, 105), - top: clamp(height * 0.14, 38, 70), - bottom: clamp(height * 0.025, 8, 14), - }), - iterations: 32, - marks: ({ chart, nodes: sankeyNodes, links: sankeyLinks }) => { - const flowMarks = [ - link(sankeyLinks, { - id: 'links', - x1: 'x1', - y1: 'y1', - x2: 'x2', - y2: 'y2', - key: 'key', - stroke: (flow) => linkColors[flow.data.tone], - strokeOpacity: (flow) => - flow.data.tone === 'Neutral' ? 0.58 : 0.64, - strokeWidth: (flow) => Math.max(1, flow.width), - lineCap: 'butt', - curve: d3Curve(curveBumpX), - }), - rect(sankeyNodes, { - id: 'nodes', - x1: 'x0', - x2: 'x1', - y1: 'y0', - y2: 'y1', - key: 'key', - color: (node) => node.data.tone, - inset: 0, - }), - ] as const - if (input.preview === true) return flowMarks - - const labelFontSize = clamp(chart.width * 0.013, 6.5, 10.5) - const labelOffset = clamp(chart.width * 0.008, 3, 6) - const labelRows = sankeyNodes.map((node): IncomeSankeyLabelRow => { - const labelAnchor = - node.data.labelSide === 'right' ? 'start' : 'end' - const labelX = - node.data.labelSide === 'right' - ? node.x1 + labelOffset - : node.x0 - labelOffset - const labelText = - chart.width < 720 && node.data.compactLabel - ? node.data.compactLabel - : node.data.label - const backdrop = labelBackdropBounds({ - anchor: labelAnchor, - centerY: node.y, - fontSize: labelFontSize, - label: labelText, - labelX, - value: node.data.displayValue, - }) - return { - ...node, - labelText, - labelX, - labelNameY: node.y - labelFontSize * 0.5, - labelValueY: node.y + labelFontSize * 0.58, - labelAnchor, - backdropX0: backdrop.x, - backdropX1: backdrop.x + backdrop.width, - backdropY0: backdrop.y, - backdropY1: backdrop.y + backdrop.height, - } - }) - const backdropRows = labelRows.filter( - (node) => node.data.labelBackdrop, - ) - const titleRows: readonly IncomeSankeyTitleRow[] = [ - { - kind: 'title', - id: 'title', - title: incomeStatementTitle, - x: chart.x + chart.width / 2, - y: chart.y + clamp(chart.height * 0.065, 17, 32), - }, - ] - - return [ - ...flowMarks, - rect(backdropRows, { - id: 'label-backdrops', - x1: 'backdropX0', - x2: 'backdropX1', - y1: 'backdropY0', - y2: 'backdropY1', - key: 'key', - fill: 'var(--panel, #ffffff)', - fillOpacity: 0.82, - inset: 0, - radius: 1, - }), - text(labelRows, { - id: 'label-names', - x: 'labelX', - y: 'labelNameY', - text: 'labelText', - key: 'key', - anchor: (node) => node.labelAnchor, - fill: 'currentColor', - fontSize: labelFontSize, - fontWeight: 700, - }), - text(labelRows, { - id: 'label-values', - x: 'labelX', - y: 'labelValueY', - text: (node) => node.data.displayValue, - key: 'key', - anchor: (node) => node.labelAnchor, - fill: 'currentColor', - fontSize: labelFontSize, - fontWeight: 500, - }), - text(titleRows, { - id: 'title', - x: 'x', - y: 'y', - text: 'title', - key: 'id', - fill: '#155477', - fontSize: clamp(chart.width * 0.034, 14, 26), - fontWeight: 750, - }), - ] as const - }, - }), - ], - color: { - domain: toneDomain, - range: toneDomain.map((tone) => toneColors[tone]), - }, - guides: false, - margin: 0, - }) -} - -function clamp(value: number, minimum: number, maximum: number) { - return Math.min(maximum, Math.max(minimum, value)) -} - -export const mount = tanstackMount(sankeyDefinition, incomeStatementTitle, { - format: ({ datum }) => { - if (datum.kind === 'title') return datum.title - if (datum.kind === 'node') { - return `${datum.data.label} · ${datum.data.displayValue}` - } - return `${datum.sourceNode.data.label} → ${datum.targetNode.data.label} · ${datum.value}` - }, -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/112-motion-entrance/controls.tsx b/benchmarks/conformance/cases/112-motion-entrance/controls.tsx new file mode 100644 index 00000000..7c6af895 --- /dev/null +++ b/benchmarks/conformance/cases/112-motion-entrance/controls.tsx @@ -0,0 +1,96 @@ +import { forwardRef, type ReactNode } from 'react' + +export const ControlBar = forwardRef< + HTMLDivElement, + { label: string; children: ReactNode } +>(function ControlBar({ label, children }, ref) { + return ( +
+ {children} +
+ ) +}) + +export function ControlField({ + children, + label, +}: { + children: ReactNode + label: string +}) { + return ( + + ) +} + +export const ControlButton = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function ControlButton({ children, style, type = 'button', ...props }, ref) { + return ( + + ) +}) + +export function RangeField({ + label, + max, + min, + onChange, + step, + suffix = '', + value, +}: { + label: string + max: number + min: number + onChange: (value: number) => void + step: number + suffix?: string + value: number +}) { + return ( + + onChange(Number(event.currentTarget.value))} + style={{ width: 96 }} + /> + {`${value}${suffix}`} + + ) +} diff --git a/benchmarks/conformance/cases/112-motion-entrance/example.tsx b/benchmarks/conformance/cases/112-motion-entrance/example.tsx new file mode 100644 index 00000000..cbd66571 --- /dev/null +++ b/benchmarks/conformance/cases/112-motion-entrance/example.tsx @@ -0,0 +1,190 @@ +import { useMemo, useRef, useState } from 'react' +import { barY, defineChart, lineY } from '@tanstack/charts' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { scaleBand, scaleLinear } from 'd3-scale' +import { ControlBar, ControlButton, ControlField, RangeField } from './controls' +import { entranceRows as rows } from './model' +import type { ChartMotionTweenTransition } from '@tanstack/charts' +import type { MotionRow } from './model' + +export interface MotionSettings { + duration: number + staggerMs: number + easing: ChartMotionTweenTransition['easing'] + customTiming: boolean +} + +const initialSettings: MotionSettings = { + duration: 1_100, + staggerMs: 55, + easing: undefined, + customTiming: true, +} + +export function motionEntranceDefinition(settings: MotionSettings) { + const { duration, easing, staggerMs, customTiming } = settings + return defineChart({ + motion: { + transition: { type: 'tween', duration, easing }, + }, + marks: [ + barY(rows, { + x: 'period', + y: 'actual', + key: 'id', + fill: '#7c3aed', + radius: 7, + inset: 4, + motion(context) { + if (customTiming && context.datum?.featured) { + return { + delay: duration * 0.19, + transition: { type: 'tween', duration: duration * 0.64 }, + } + } + return { delay: context.datumIndex * staggerMs } + }, + }), + lineY(rows, { + x: 'period', + y: 'target', + key: 'id', + stroke: '#f97316', + strokeWidth: 3, + motion: customTiming + ? { + delay: duration * 0.1, + transition: { type: 'tween', duration: duration * 0.78 }, + } + : undefined, + }), + ], + x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, + y: { scale: scaleLinear().domain([0, 100]) }, + guides: false, + margin: { top: 20, right: 20, bottom: 20, left: 20 }, + }) +} + +function readEasing(value: string): MotionSettings['easing'] { + return value === 'linear' || + value === 'ease' || + value === 'ease-in' || + value === 'ease-out' || + value === 'ease-in-out' + ? value + : undefined +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function MotionEntranceExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '112-motion-entrance' + const viewRef = useRef(null) + + const replayRef = useRef(null) + + const [settings, setSettings] = useState(initialSettings) + + const [replayCount, setReplayCount] = useState(1) + + const renderer = useMemo( + () => motion(), + [replayCount], + ) + + const definition = useMemo( + () => motionEntranceDefinition(settings), + [settings], + ) + + const replay = () => setReplayCount((value) => value + 1) + + const changeSettings = (next: Partial) => { + setSettings((current) => ({ ...current, ...next })) + replay() + } + + return ( +
+ + changeSettings({ duration })} + /> + changeSettings({ staggerMs })} + /> + + + + + + changeSettings({ customTiming: event.currentTarget.checked }) + } + /> + + + Replay + + + +
+ ) +} diff --git a/benchmarks/conformance/cases/112-motion-entrance/tanstack.test.ts b/benchmarks/conformance/cases/112-motion-entrance/tanstack.test.ts index 02390b06..0fed9556 100644 --- a/benchmarks/conformance/cases/112-motion-entrance/tanstack.test.ts +++ b/benchmarks/conformance/cases/112-motion-entrance/tanstack.test.ts @@ -40,22 +40,27 @@ describe('declarative entrance motion', () => { }) it('leaves only replay controls and renderer observation outside the definition', () => { - const source = readFileSync( + const shellSource = readFileSync( resolve( process.cwd(), 'benchmarks/conformance/cases/112-motion-entrance/view.tsx', ), 'utf8', ) - const start = source.indexOf( - 'export function motionEntranceDefinition(settings: MotionSettings)', + const definitionSource = readFileSync( + resolve( + process.cwd(), + 'benchmarks/conformance/cases/112-motion-entrance/example.tsx', + ), + 'utf8', ) - const end = source.indexOf('function readEasing', start) - const definitionSource = source.slice(start, end) - expect(source).toContain("from '../../shared/motion'") - expect(source).toContain('') - expect(source).toContain('settleChartMotion(viewRef.current') + expect(shellSource).toContain("from './example'") + expect(shellSource).toContain("from '../../shared/motion'") + expect(shellSource).toContain( + '', + ) + expect(shellSource).toContain('settleChartMotion(viewRef.current') expect(definitionSource).toContain('defineChart({') expect(definitionSource).toContain('motion: {') expect(definitionSource).toContain('barY(rows, {') diff --git a/benchmarks/conformance/cases/112-motion-entrance/tanstack.ts b/benchmarks/conformance/cases/112-motion-entrance/tanstack.ts index 8fa643ea..437f7e7a 100644 --- a/benchmarks/conformance/cases/112-motion-entrance/tanstack.ts +++ b/benchmarks/conformance/cases/112-motion-entrance/tanstack.ts @@ -1,2 +1,2 @@ -export { catalogCase, motionEntranceDefinition, mount } from './view' -export type { MotionSettings } from './view' +export * from './example' +export { catalogCase, mount } from './view' diff --git a/benchmarks/conformance/cases/112-motion-entrance/view.tsx b/benchmarks/conformance/cases/112-motion-entrance/view.tsx index 295c9eb4..9814bc47 100644 --- a/benchmarks/conformance/cases/112-motion-entrance/view.tsx +++ b/benchmarks/conformance/cases/112-motion-entrance/view.tsx @@ -1,3 +1,5 @@ +import { motionEntranceDefinition } from './example' +import type { MotionSettings } from './example' import { forwardRef, useImperativeHandle, @@ -5,10 +7,8 @@ import { useRef, useState, } from 'react' -import { barY, defineChart, lineY } from '@tanstack/charts' import { motion } from '@tanstack/charts/motion' import { Chart } from '@tanstack/charts/react/core' -import { scaleBand, scaleLinear } from 'd3-scale' import { readChartMotionState, settleChartMotion } from '../../shared/motion' import { ControlBar, @@ -18,19 +18,10 @@ import { } from '../../shared/react-controls' import { reactMount } from '../../shared/react-mount' import { tanstackCase } from '../../shared/mount' -import { entranceRows as rows } from './model' -import type { ChartMotionTweenTransition } from '@tanstack/charts' import type { MotionRow } from './model' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' -export interface MotionSettings { - duration: number - staggerMs: number - easing: ChartMotionTweenTransition['easing'] - customTiming: boolean -} - const initialSettings: MotionSettings = { duration: 1_100, staggerMs: 55, @@ -167,51 +158,6 @@ const MotionEntranceExample = forwardRef< export const mount = reactMount(MotionEntranceExample) -export function motionEntranceDefinition(settings: MotionSettings) { - const { duration, easing, staggerMs, customTiming } = settings - return defineChart({ - motion: { - transition: { type: 'tween', duration, easing }, - }, - marks: [ - barY(rows, { - x: 'period', - y: 'actual', - key: 'id', - fill: '#7c3aed', - radius: 7, - inset: 4, - motion(context) { - if (customTiming && context.datum?.featured) { - return { - delay: duration * 0.19, - transition: { type: 'tween', duration: duration * 0.64 }, - } - } - return { delay: context.datumIndex * staggerMs } - }, - }), - lineY(rows, { - x: 'period', - y: 'target', - key: 'id', - stroke: '#f97316', - strokeWidth: 3, - motion: customTiming - ? { - delay: duration * 0.1, - transition: { type: 'tween', duration: duration * 0.78 }, - } - : undefined, - }), - ], - x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, - y: { scale: scaleLinear().domain([0, 100]) }, - guides: false, - margin: { top: 20, right: 20, bottom: 20, left: 20 }, - }) -} - export const catalogCase = tanstackCase( () => motionEntranceDefinition(initialSettings), 'Staggered monthly actuals and target', diff --git a/benchmarks/conformance/cases/113-motion-updates/controls.tsx b/benchmarks/conformance/cases/113-motion-updates/controls.tsx new file mode 100644 index 00000000..7c6af895 --- /dev/null +++ b/benchmarks/conformance/cases/113-motion-updates/controls.tsx @@ -0,0 +1,96 @@ +import { forwardRef, type ReactNode } from 'react' + +export const ControlBar = forwardRef< + HTMLDivElement, + { label: string; children: ReactNode } +>(function ControlBar({ label, children }, ref) { + return ( +
+ {children} +
+ ) +}) + +export function ControlField({ + children, + label, +}: { + children: ReactNode + label: string +}) { + return ( + + ) +} + +export const ControlButton = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function ControlButton({ children, style, type = 'button', ...props }, ref) { + return ( + + ) +}) + +export function RangeField({ + label, + max, + min, + onChange, + step, + suffix = '', + value, +}: { + label: string + max: number + min: number + onChange: (value: number) => void + step: number + suffix?: string + value: number +}) { + return ( + + onChange(Number(event.currentTarget.value))} + style={{ width: 96 }} + /> + {`${value}${suffix}`} + + ) +} diff --git a/benchmarks/conformance/cases/113-motion-updates/example.tsx b/benchmarks/conformance/cases/113-motion-updates/example.tsx new file mode 100644 index 00000000..438b7557 --- /dev/null +++ b/benchmarks/conformance/cases/113-motion-updates/example.tsx @@ -0,0 +1,343 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { ControlBar, ControlButton, ControlField, RangeField } from './controls' +import { updateStages as stages } from './model' +import type { UpdateRow } from './model' +import { barY, defineChart, lineY } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import type { ChartMotionTweenTransition } from '@tanstack/charts/motion' +const initialSettings: UpdateSettings = { + duration: 1_100, + easing: undefined, + spring: false, + stiffness: 170, + damping: 14, + mass: 1, +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export interface UpdateSettings { + duration: number + easing: ChartMotionTweenTransition['easing'] + spring: boolean + stiffness: number + damping: number + mass: number +} + +export function motionUpdatesDefinition( + rows: readonly UpdateRow[], + settings: UpdateSettings, +) { + return defineChart({ + motion: { + transition: settings.spring + ? { + type: 'spring', + stiffness: settings.stiffness, + damping: settings.damping, + mass: settings.mass, + } + : { + type: 'tween', + duration: settings.duration, + easing: settings.easing, + }, + }, + marks: [ + barY(rows, { + id: 'actual', + x: 'period', + y: 'actual', + key: 'id', + fill: '#7c3aed', + radius: 7, + inset: 4, + motion(context) { + if (settings.spring) { + if (context.phase === 'exit') { + return { + transition: { + type: 'spring', + stiffness: settings.stiffness * 1.25, + damping: settings.damping * 1.15, + }, + } + } + if (context.datum?.featured) { + return { + delay: context.phase === 'enter' ? 70 : 0, + transition: { + type: 'spring', + mass: settings.mass * 1.35, + }, + } + } + return undefined + } + if (context.phase === 'exit') { + return { + transition: { + type: 'tween', + duration: settings.duration * 0.45, + }, + } + } + if (context.datum?.featured) { + return { + delay: settings.duration * 0.16, + transition: { + type: 'tween', + duration: settings.duration * 0.6, + }, + } + } + return undefined + }, + }), + lineY(rows, { + id: 'target', + x: 'period', + y: 'target', + key: 'id', + stroke: '#f97316', + strokeWidth: 3, + motion: { + delay: 80, + transition: settings.spring + ? { + type: 'spring', + stiffness: settings.stiffness * 0.78, + } + : { + type: 'tween', + duration: settings.duration * 0.82, + }, + }, + }), + ], + x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, + y: { scale: scaleLinear().domain([0, 100]) }, + guides: false, + margin: { top: 20, right: 20, bottom: 20, left: 20 }, + maxFocusDistance: 28, + }) +} + +export function readEasing(value: string): UpdateSettings['easing'] { + return value === 'linear' || + value === 'ease' || + value === 'ease-in' || + value === 'ease-out' || + value === 'ease-in-out' + ? value + : undefined +} + +export function springRegime(settings: UpdateSettings) { + const ratio = + settings.damping / (2 * Math.sqrt(settings.stiffness * settings.mass)) + if (ratio < 0.99) return 'underdamped' + if (ratio > 1.01) return 'overdamped' + return 'critical' +} + +export default function MotionUpdatesExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '113-motion-updates' + const viewRef = useRef(null) + + const updateRef = useRef(null) + + const interruptRef = useRef(null) + + const replayRef = useRef(null) + + const timerRef = useRef(undefined) + + const [stage, setStage] = useState( + () => Math.abs(input.revision) % stages.length, + ) + + const [settings, setSettings] = useState(initialSettings) + + const [replayCount, setReplayCount] = useState(1) + + const [, setInterruptionCount] = useState(0) + + const [announcement, setAnnouncement] = useState('') + + const renderer = useMemo( + () => motion(), + [replayCount], + ) + + const rows = stages[stage] ?? stages[0] + + const definition = useMemo( + () => motionUpdatesDefinition(rows, settings), + [rows, settings], + ) + + const clearTimer = () => { + if (timerRef.current !== undefined) window.clearTimeout(timerRef.current) + timerRef.current = undefined + } + + const rebuild = (next: Partial) => { + clearTimer() + setSettings((current) => ({ ...current, ...next })) + setReplayCount((value) => value + 1) + setAnnouncement('') + } + + const advance = () => { + clearTimer() + setStage((value) => (value + 1) % stages.length) + setAnnouncement('') + } + + const interrupt = () => { + clearTimer() + setStage(1) + setAnnouncement('Interrupting in 400 ms') + timerRef.current = window.setTimeout(() => { + setStage(2) + setInterruptionCount((value) => value + 1) + setAnnouncement('') + timerRef.current = undefined + }, 400) + } + + const replay = () => { + clearTimer() + setStage(0) + setReplayCount((value) => value + 1) + setAnnouncement('') + } + + useEffect(() => { + clearTimer() + setStage(Math.abs(input.revision) % stages.length) + setAnnouncement('') + }, [input.revision]) + + useEffect(() => () => clearTimer(), []) + + return ( +
+ + {settings.spring ? null : ( + rebuild({ duration })} + /> + )} + + + + {settings.spring ? ( + <> + rebuild({ stiffness })} + /> + rebuild({ damping })} + /> + rebuild({ mass })} + /> + + {springRegime(settings)} · momentum preserved + + + ) : null} + + Update + + + Interrupt + + + Replay + + + {announcement || `Stage ${stage + 1} of ${stages.length}`} + + + +
+ ) +} diff --git a/benchmarks/conformance/cases/113-motion-updates/tanstack.test.ts b/benchmarks/conformance/cases/113-motion-updates/tanstack.test.ts index af7022ee..28b295f3 100644 --- a/benchmarks/conformance/cases/113-motion-updates/tanstack.test.ts +++ b/benchmarks/conformance/cases/113-motion-updates/tanstack.test.ts @@ -198,7 +198,7 @@ describe('declarative keyed motion updates', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/113-motion-updates/tanstack.ts', + 'benchmarks/conformance/cases/113-motion-updates/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/113-motion-updates/tanstack.ts b/benchmarks/conformance/cases/113-motion-updates/tanstack.ts index bd2dbf77..cccbe3f1 100644 --- a/benchmarks/conformance/cases/113-motion-updates/tanstack.ts +++ b/benchmarks/conformance/cases/113-motion-updates/tanstack.ts @@ -1,137 +1,13 @@ -import { barY, defineChart, lineY } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' +import { motionUpdatesDefinition, readEasing, springRegime } from './example' +import type { UpdateSettings } from './example' +export { motionUpdatesDefinition, readEasing, springRegime } from './example' +export type { UpdateSettings } from './example' import { updateStages as stages } from './model' import { tanstackCase } from '../../shared/mount' -import type { ChartMotionTweenTransition } from '@tanstack/charts/motion' -import type { UpdateRow } from './model' -export { mount } from './view' - -export interface UpdateSettings { - duration: number - easing: ChartMotionTweenTransition['easing'] - spring: boolean - stiffness: number - damping: number - mass: number -} - -export function motionUpdatesDefinition( - rows: readonly UpdateRow[], - settings: UpdateSettings, -) { - return defineChart({ - motion: { - transition: settings.spring - ? { - type: 'spring', - stiffness: settings.stiffness, - damping: settings.damping, - mass: settings.mass, - } - : { - type: 'tween', - duration: settings.duration, - easing: settings.easing, - }, - }, - marks: [ - barY(rows, { - id: 'actual', - x: 'period', - y: 'actual', - key: 'id', - fill: '#7c3aed', - radius: 7, - inset: 4, - motion(context) { - if (settings.spring) { - if (context.phase === 'exit') { - return { - transition: { - type: 'spring', - stiffness: settings.stiffness * 1.25, - damping: settings.damping * 1.15, - }, - } - } - if (context.datum?.featured) { - return { - delay: context.phase === 'enter' ? 70 : 0, - transition: { - type: 'spring', - mass: settings.mass * 1.35, - }, - } - } - return undefined - } - if (context.phase === 'exit') { - return { - transition: { - type: 'tween', - duration: settings.duration * 0.45, - }, - } - } - if (context.datum?.featured) { - return { - delay: settings.duration * 0.16, - transition: { - type: 'tween', - duration: settings.duration * 0.6, - }, - } - } - return undefined - }, - }), - lineY(rows, { - id: 'target', - x: 'period', - y: 'target', - key: 'id', - stroke: '#f97316', - strokeWidth: 3, - motion: { - delay: 80, - transition: settings.spring - ? { - type: 'spring', - stiffness: settings.stiffness * 0.78, - } - : { - type: 'tween', - duration: settings.duration * 0.82, - }, - }, - }), - ], - x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, - y: { scale: scaleLinear().domain([0, 100]) }, - guides: false, - margin: { top: 20, right: 20, bottom: 20, left: 20 }, - maxFocusDistance: 28, - }) -} +export { default as Example } from './example' -export function readEasing(value: string): UpdateSettings['easing'] { - return value === 'linear' || - value === 'ease' || - value === 'ease-in' || - value === 'ease-out' || - value === 'ease-in-out' - ? value - : undefined -} - -export function springRegime(settings: UpdateSettings) { - const ratio = - settings.damping / (2 * Math.sqrt(settings.stiffness * settings.mass)) - if (ratio < 0.99) return 'underdamped' - if (ratio > 1.01) return 'overdamped' - return 'critical' -} +export { mount } from './view' export const catalogCase = tanstackCase( (input) => diff --git a/benchmarks/conformance/cases/113-motion-updates/view.tsx b/benchmarks/conformance/cases/113-motion-updates/view.tsx index 7c41d8ed..796f38bf 100644 --- a/benchmarks/conformance/cases/113-motion-updates/view.tsx +++ b/benchmarks/conformance/cases/113-motion-updates/view.tsx @@ -16,10 +16,10 @@ import { RangeField, } from '../../shared/react-controls' import { reactMount } from '../../shared/react-mount' -import { motionUpdatesDefinition, readEasing, springRegime } from './tanstack' +import { motionUpdatesDefinition, readEasing, springRegime } from './example' import { updateStages as stages } from './model' import type { UpdateRow } from './model' -import type { UpdateSettings } from './tanstack' +import type { UpdateSettings } from './example' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/114-spring-line-motion/controls.tsx b/benchmarks/conformance/cases/114-spring-line-motion/controls.tsx new file mode 100644 index 00000000..7c6af895 --- /dev/null +++ b/benchmarks/conformance/cases/114-spring-line-motion/controls.tsx @@ -0,0 +1,96 @@ +import { forwardRef, type ReactNode } from 'react' + +export const ControlBar = forwardRef< + HTMLDivElement, + { label: string; children: ReactNode } +>(function ControlBar({ label, children }, ref) { + return ( +
+ {children} +
+ ) +}) + +export function ControlField({ + children, + label, +}: { + children: ReactNode + label: string +}) { + return ( + + ) +} + +export const ControlButton = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function ControlButton({ children, style, type = 'button', ...props }, ref) { + return ( + + ) +}) + +export function RangeField({ + label, + max, + min, + onChange, + step, + suffix = '', + value, +}: { + label: string + max: number + min: number + onChange: (value: number) => void + step: number + suffix?: string + value: number +}) { + return ( + + onChange(Number(event.currentTarget.value))} + style={{ width: 96 }} + /> + {`${value}${suffix}`} + + ) +} diff --git a/benchmarks/conformance/cases/114-spring-line-motion/example.tsx b/benchmarks/conformance/cases/114-spring-line-motion/example.tsx new file mode 100644 index 00000000..d6997a0f --- /dev/null +++ b/benchmarks/conformance/cases/114-spring-line-motion/example.tsx @@ -0,0 +1,199 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { ControlBar, ControlButton, ControlField } from './controls' +import { springLineStages } from './model' +import type { SpringLineRow } from './model' +import { defineChart, lineY } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export type SpringLineTransitionMode = 'spring' | 'tween' + +export function springLineMotionDefinition( + rows: readonly SpringLineRow[], + mode: SpringLineTransitionMode, +) { + return defineChart({ + motion: { + transition: + mode === 'spring' + ? { type: 'spring', stiffness: 170, damping: 18, mass: 1 } + : { type: 'tween', duration: 650, easing: 'ease-out' }, + }, + marks: [ + lineY(rows, { + id: 'primary', + x: 'period', + y: 'primary', + key: 'id', + stroke: '#7c3aed', + strokeWidth: 4, + }), + lineY(rows, { + id: 'comparison', + x: 'period', + y: 'comparison', + key: 'id', + stroke: '#f97316', + strokeWidth: 3, + motion(context) { + return { + delay: context.phase === 'enter' ? 90 : 0, + transition: + mode === 'spring' + ? { type: 'spring', mass: 1.2 } + : { + type: 'tween', + duration: 820, + easing: 'ease-in-out', + }, + } + }, + }), + ], + x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, + y: { scale: scaleLinear().domain([0, 100]) }, + guides: false, + margin: { top: 24, right: 24, bottom: 24, left: 24 }, + }) +} + +export default function SpringLineMotionExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '114-spring-line-motion' + const viewRef = useRef(null) + + const updateRef = useRef(null) + + const interruptRef = useRef(null) + + const replayRef = useRef(null) + + const timerRef = useRef(undefined) + + const [stage, setStage] = useState( + () => Math.abs(input.revision) % springLineStages.length, + ) + + const [mode, setMode] = useState('spring') + + const [replayCount, setReplayCount] = useState(1) + + const [, setInterruptionCount] = useState(0) + + const [announcement, setAnnouncement] = useState('') + + const renderer = useMemo( + () => motion(), + [replayCount], + ) + + const definition = useMemo( + () => + springLineMotionDefinition( + springLineStages[stage] ?? springLineStages[0], + mode, + ), + [mode, stage], + ) + + const clearTimer = () => { + if (timerRef.current !== undefined) window.clearTimeout(timerRef.current) + timerRef.current = undefined + } + + const update = () => { + clearTimer() + setStage((value) => (value + 1) % springLineStages.length) + setAnnouncement('') + } + + const interrupt = () => { + clearTimer() + setStage(1) + setAnnouncement('Reversing in 260 ms') + timerRef.current = window.setTimeout(() => { + setStage(2) + setInterruptionCount((value) => value + 1) + setAnnouncement('') + timerRef.current = undefined + }, 260) + } + + const replay = () => { + clearTimer() + setStage(0) + setReplayCount((value) => value + 1) + setAnnouncement('') + } + + useEffect(() => { + clearTimer() + setStage(Math.abs(input.revision) % springLineStages.length) + setAnnouncement('') + }, [input.revision]) + + useEffect(() => () => clearTimer(), []) + + return ( +
+ + + + + + Update + + + Interrupt + + + Replay + + + {announcement || `Stage ${stage + 1} of ${springLineStages.length}`} + + + +
+ ) +} diff --git a/benchmarks/conformance/cases/114-spring-line-motion/tanstack.test.ts b/benchmarks/conformance/cases/114-spring-line-motion/tanstack.test.ts index ed6fdb87..d615444c 100644 --- a/benchmarks/conformance/cases/114-spring-line-motion/tanstack.test.ts +++ b/benchmarks/conformance/cases/114-spring-line-motion/tanstack.test.ts @@ -165,12 +165,12 @@ describe('declarative spring line motion', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts', + 'benchmarks/conformance/cases/114-spring-line-motion/example.tsx', ), 'utf8', ) const start = source.indexOf('export function springLineMotionDefinition(') - const end = source.length + const end = source.indexOf('export default function', start) const definitionSource = source.slice(start, end) const view = readFileSync( diff --git a/benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts b/benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts index e0c23abf..cd560235 100644 --- a/benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts +++ b/benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts @@ -1,61 +1,13 @@ -import { defineChart, lineY } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' +import { springLineMotionDefinition } from './example' +import type { SpringLineTransitionMode } from './example' +export { springLineMotionDefinition } from './example' +export type { SpringLineTransitionMode } from './example' import { springLineStages } from './model' import { tanstackCase } from '../../shared/mount' -import type { SpringLineRow } from './model' -export { mount } from './view' - -export type SpringLineTransitionMode = 'spring' | 'tween' +export { default as Example } from './example' -export function springLineMotionDefinition( - rows: readonly SpringLineRow[], - mode: SpringLineTransitionMode, -) { - return defineChart({ - motion: { - transition: - mode === 'spring' - ? { type: 'spring', stiffness: 170, damping: 18, mass: 1 } - : { type: 'tween', duration: 650, easing: 'ease-out' }, - }, - marks: [ - lineY(rows, { - id: 'primary', - x: 'period', - y: 'primary', - key: 'id', - stroke: '#7c3aed', - strokeWidth: 4, - }), - lineY(rows, { - id: 'comparison', - x: 'period', - y: 'comparison', - key: 'id', - stroke: '#f97316', - strokeWidth: 3, - motion(context) { - return { - delay: context.phase === 'enter' ? 90 : 0, - transition: - mode === 'spring' - ? { type: 'spring', mass: 1.2 } - : { - type: 'tween', - duration: 820, - easing: 'ease-in-out', - }, - } - }, - }), - ], - x: { scale: scaleBand().domain(rows.map((row) => row.period)) }, - y: { scale: scaleLinear().domain([0, 100]) }, - guides: false, - margin: { top: 24, right: 24, bottom: 24, left: 24 }, - }) -} +export { mount } from './view' export const catalogCase = tanstackCase( (input) => diff --git a/benchmarks/conformance/cases/114-spring-line-motion/view.tsx b/benchmarks/conformance/cases/114-spring-line-motion/view.tsx index 2932a31f..61baa135 100644 --- a/benchmarks/conformance/cases/114-spring-line-motion/view.tsx +++ b/benchmarks/conformance/cases/114-spring-line-motion/view.tsx @@ -16,9 +16,9 @@ import { } from '../../shared/react-controls' import { reactMount } from '../../shared/react-mount' import { springLineStages } from './model' -import { springLineMotionDefinition } from './tanstack' +import { springLineMotionDefinition } from './example' import type { SpringLineRow } from './model' -import type { SpringLineTransitionMode } from './tanstack' +import type { SpringLineTransitionMode } from './example' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/115-definition-motion/controls.tsx b/benchmarks/conformance/cases/115-definition-motion/controls.tsx new file mode 100644 index 00000000..7c6af895 --- /dev/null +++ b/benchmarks/conformance/cases/115-definition-motion/controls.tsx @@ -0,0 +1,96 @@ +import { forwardRef, type ReactNode } from 'react' + +export const ControlBar = forwardRef< + HTMLDivElement, + { label: string; children: ReactNode } +>(function ControlBar({ label, children }, ref) { + return ( +
+ {children} +
+ ) +}) + +export function ControlField({ + children, + label, +}: { + children: ReactNode + label: string +}) { + return ( + + ) +} + +export const ControlButton = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function ControlButton({ children, style, type = 'button', ...props }, ref) { + return ( + + ) +}) + +export function RangeField({ + label, + max, + min, + onChange, + step, + suffix = '', + value, +}: { + label: string + max: number + min: number + onChange: (value: number) => void + step: number + suffix?: string + value: number +}) { + return ( + + onChange(Number(event.currentTarget.value))} + style={{ width: 96 }} + /> + {`${value}${suffix}`} + + ) +} diff --git a/benchmarks/conformance/cases/115-definition-motion/example.tsx b/benchmarks/conformance/cases/115-definition-motion/example.tsx new file mode 100644 index 00000000..0631c044 --- /dev/null +++ b/benchmarks/conformance/cases/115-definition-motion/example.tsx @@ -0,0 +1,216 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { ControlBar, ControlButton } from './controls' +import { definitionMotionStages } from './model' +import type { DefinitionMotionRow } from './model' +import { barY, defineChart, lineY } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export function definitionMotionDefinition( + rows: readonly DefinitionMotionRow[], + preview = false, +) { + const maximum = Math.max(100, ...rows.map((row) => row.actual)) + const yMaximum = Math.ceil(maximum / 20) * 20 + const guideMotion = { + transition: { + type: 'tween' as const, + duration: 260, + easing: 'ease-out' as const, + }, + } + return defineChart({ + motion: { + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }, + marks: [ + barY(rows, { + id: 'actual', + x: 'period', + y: 'actual', + key: 'id', + fill: '#7c3aed', + radius: 6, + inset: 5, + motion(context) { + return { + delay: context.phase === 'enter' ? context.datumIndex * 34 : 0, + transition: context.datum?.featured + ? { type: 'spring', mass: 1.45 } + : undefined, + } + }, + }), + lineY(rows, { + id: 'target', + x: 'period', + y: 'target', + key: 'id', + stroke: '#f97316', + strokeWidth: 3, + motion: { + transition: { + type: 'tween', + duration: 520, + easing: 'ease-in-out', + }, + }, + }), + ], + x: { + scale: scaleBand().domain(rows.map((row) => row.period)), + axis: { + motion: guideMotion, + ticks: { motion: guideMotion }, + tickLabels: { + motion(context) { + return { + delay: context.datumIndex * 18, + transition: { type: 'tween', duration: 220 }, + } + }, + }, + label: { text: 'Period', motion: guideMotion }, + }, + }, + y: { + scale: scaleLinear().domain([0, yMaximum]), + grid: true, + axis: { + motion: guideMotion, + ticks: { motion: guideMotion }, + tickLabels: { motion: guideMotion }, + label: { text: 'Value', motion: guideMotion }, + }, + }, + margin: preview + ? { top: 12, right: 4, bottom: 40, left: 46 } + : { top: 20, right: 24 }, + maxFocusDistance: 32, + }) +} + +export default function DefinitionMotionExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '115-definition-motion' + const viewRef = useRef(null) + + const updateRef = useRef(null) + + const interruptRef = useRef(null) + + const replayRef = useRef(null) + + const timerRef = useRef(undefined) + + const [stage, setStage] = useState( + () => Math.abs(input.revision) % definitionMotionStages.length, + ) + + const [replayCount, setReplayCount] = useState(1) + + const [, setInterruptionCount] = useState(0) + + const [announcement, setAnnouncement] = useState('') + + const renderer = useMemo( + () => motion(), + [replayCount], + ) + + const definition = useMemo( + () => + definitionMotionDefinition( + definitionMotionStages[stage] ?? definitionMotionStages[0], + ), + [stage], + ) + + const clearTimer = () => { + if (timerRef.current !== undefined) window.clearTimeout(timerRef.current) + timerRef.current = undefined + } + + const advance = () => { + clearTimer() + setStage((value) => (value + 1) % definitionMotionStages.length) + setAnnouncement('') + } + + const interrupt = () => { + clearTimer() + setStage(1) + setAnnouncement('Retargeting in 220 ms') + timerRef.current = window.setTimeout(() => { + setStage(2) + setInterruptionCount((value) => value + 1) + setAnnouncement('') + timerRef.current = undefined + }, 220) + } + + const replay = () => { + clearTimer() + setStage(0) + setReplayCount((value) => value + 1) + setAnnouncement('') + } + + useEffect(() => { + clearTimer() + setStage(Math.abs(input.revision) % definitionMotionStages.length) + setAnnouncement('') + }, [input.revision]) + + useEffect(() => () => clearTimer(), []) + + return ( +
+ + + Update + + + Interrupt + + + Replay + + + {announcement || + `Stage ${stage + 1} of ${definitionMotionStages.length}`} + + + +
+ ) +} diff --git a/benchmarks/conformance/cases/115-definition-motion/tanstack.test.ts b/benchmarks/conformance/cases/115-definition-motion/tanstack.test.ts index 9f6c451c..fd95b074 100644 --- a/benchmarks/conformance/cases/115-definition-motion/tanstack.test.ts +++ b/benchmarks/conformance/cases/115-definition-motion/tanstack.test.ts @@ -182,12 +182,12 @@ describe('declarative definition-owned motion', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/115-definition-motion/tanstack.ts', + 'benchmarks/conformance/cases/115-definition-motion/example.tsx', ), 'utf8', ) const start = source.indexOf('export function definitionMotionDefinition(') - const end = source.length + const end = source.indexOf('export default function', start) const definitionSource = source.slice(start, end) const view = readFileSync( diff --git a/benchmarks/conformance/cases/115-definition-motion/tanstack.ts b/benchmarks/conformance/cases/115-definition-motion/tanstack.ts index e38f6bfe..017877fa 100644 --- a/benchmarks/conformance/cases/115-definition-motion/tanstack.ts +++ b/benchmarks/conformance/cases/115-definition-motion/tanstack.ts @@ -1,94 +1,11 @@ -import { barY, defineChart, lineY } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' +import { definitionMotionDefinition } from './example' +export { definitionMotionDefinition } from './example' import { definitionMotionStages } from './model' import { tanstackCase } from '../../shared/mount' -import type { DefinitionMotionRow } from './model' -export { mount } from './view' +export { default as Example } from './example' -export function definitionMotionDefinition( - rows: readonly DefinitionMotionRow[], - preview = false, -) { - const maximum = Math.max(100, ...rows.map((row) => row.actual)) - const yMaximum = Math.ceil(maximum / 20) * 20 - const guideMotion = { - transition: { - type: 'tween' as const, - duration: 260, - easing: 'ease-out' as const, - }, - } - return defineChart({ - motion: { - transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, - }, - marks: [ - barY(rows, { - id: 'actual', - x: 'period', - y: 'actual', - key: 'id', - fill: '#7c3aed', - radius: 6, - inset: 5, - motion(context) { - return { - delay: context.phase === 'enter' ? context.datumIndex * 34 : 0, - transition: context.datum?.featured - ? { type: 'spring', mass: 1.45 } - : undefined, - } - }, - }), - lineY(rows, { - id: 'target', - x: 'period', - y: 'target', - key: 'id', - stroke: '#f97316', - strokeWidth: 3, - motion: { - transition: { - type: 'tween', - duration: 520, - easing: 'ease-in-out', - }, - }, - }), - ], - x: { - scale: scaleBand().domain(rows.map((row) => row.period)), - axis: { - motion: guideMotion, - ticks: { motion: guideMotion }, - tickLabels: { - motion(context) { - return { - delay: context.datumIndex * 18, - transition: { type: 'tween', duration: 220 }, - } - }, - }, - label: { text: 'Period', motion: guideMotion }, - }, - }, - y: { - scale: scaleLinear().domain([0, yMaximum]), - grid: true, - axis: { - motion: guideMotion, - ticks: { motion: guideMotion }, - tickLabels: { motion: guideMotion }, - label: { text: 'Value', motion: guideMotion }, - }, - }, - margin: preview - ? { top: 12, right: 4, bottom: 40, left: 46 } - : { top: 20, right: 24 }, - maxFocusDistance: 32, - }) -} +export { mount } from './view' export const catalogCase = tanstackCase( (input) => diff --git a/benchmarks/conformance/cases/115-definition-motion/view.tsx b/benchmarks/conformance/cases/115-definition-motion/view.tsx index 45b9fd9f..5182e9a3 100644 --- a/benchmarks/conformance/cases/115-definition-motion/view.tsx +++ b/benchmarks/conformance/cases/115-definition-motion/view.tsx @@ -12,7 +12,7 @@ import { readChartMotionState, settleChartMotion } from '../../shared/motion' import { ControlBar, ControlButton } from '../../shared/react-controls' import { reactMount } from '../../shared/react-mount' import { definitionMotionStages } from './model' -import { definitionMotionDefinition } from './tanstack' +import { definitionMotionDefinition } from './example' import type { DefinitionMotionRow } from './model' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/116-geometry-morph/controls.tsx b/benchmarks/conformance/cases/116-geometry-morph/controls.tsx new file mode 100644 index 00000000..7c6af895 --- /dev/null +++ b/benchmarks/conformance/cases/116-geometry-morph/controls.tsx @@ -0,0 +1,96 @@ +import { forwardRef, type ReactNode } from 'react' + +export const ControlBar = forwardRef< + HTMLDivElement, + { label: string; children: ReactNode } +>(function ControlBar({ label, children }, ref) { + return ( +
+ {children} +
+ ) +}) + +export function ControlField({ + children, + label, +}: { + children: ReactNode + label: string +}) { + return ( + + ) +} + +export const ControlButton = forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(function ControlButton({ children, style, type = 'button', ...props }, ref) { + return ( + + ) +}) + +export function RangeField({ + label, + max, + min, + onChange, + step, + suffix = '', + value, +}: { + label: string + max: number + min: number + onChange: (value: number) => void + step: number + suffix?: string + value: number +}) { + return ( + + onChange(Number(event.currentTarget.value))} + style={{ width: 96 }} + /> + {`${value}${suffix}`} + + ) +} diff --git a/benchmarks/conformance/cases/116-geometry-morph/example.tsx b/benchmarks/conformance/cases/116-geometry-morph/example.tsx new file mode 100644 index 00000000..74192a21 --- /dev/null +++ b/benchmarks/conformance/cases/116-geometry-morph/example.tsx @@ -0,0 +1,428 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { ControlBar, ControlButton } from './controls' +import { morphData, morphModes } from './model' +import type { MorphDatum, MorphMode } from './model' +import { createMark, defineChart } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import type { + ChartMotionDefinition, + ChartPoint, + SceneNode, +} from '@tanstack/charts' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +type Point = readonly [number, number] + +const sampleCount = 48 + +export function geometryMorphDefinition( + data: readonly MorphDatum[], + mode: MorphMode, +) { + return defineChart({ + motion: { + transition: { + type: 'spring', + stiffness: 105, + damping: 16, + mass: 0.9, + }, + }, + marks: [ + normalizedTopologyMark(data, mode, { + id: 'geometry-morph', + key: (datum) => datum.id, + fill: (datum) => datum.color, + fillOpacity: 0.9, + stroke: 'Canvas', + strokeOpacity: 0.75, + strokeWidth: 1.5, + lineJoin: 'round', + motion(context) { + return { + delay: context.phase === 'enter' ? context.datumIndex * 38 : 0, + transition: + context.datum?.id === 'violet' + ? { type: 'spring', mass: 1.35 } + : undefined, + } + }, + }), + ], + x: { scale: scaleLinear().domain([0, data.length - 1]) }, + y: { scale: scaleLinear().domain([0, 100]) }, + guides: false, + margin: 0, + }) +} + +interface NormalizedTopologyMarkOptions { + id: string + key: (datum: MorphDatum, index: number) => string + fill: (datum: MorphDatum, index: number) => string + fillOpacity: number + stroke: string + strokeOpacity: number + strokeWidth: number + lineJoin: 'miter' | 'round' | 'bevel' + motion?: ChartMotionDefinition +} + +function normalizedTopologyMark( + data: readonly MorphDatum[], + mode: MorphMode, + options: NormalizedTopologyMarkOptions, +) { + return createMark(() => { + const id = options.id + return { + id, + channels: { + x: { scale: 'x', values: data.map((_datum, index) => index) }, + y: { + scale: 'y', + values: data.map((datum) => datum.value), + includeZero: true, + }, + }, + render: ({ chart }) => { + const geometries = geometryForMode(data, mode, chart) + const nodes: SceneNode[] = [] + const points: ChartPoint[] = [] + data.forEach((datum, datumIndex) => { + const geometry = geometries[datumIndex] + if (!geometry) return + const datumKey = options.key(datum, datumIndex) + const key = `${id}:${datumKey}` + nodes.push({ + kind: 'area', + key, + points: geometry.points, + style: { + fill: options.fill(datum, datumIndex), + fillOpacity: options.fillOpacity, + stroke: options.stroke, + strokeOpacity: options.strokeOpacity, + strokeWidth: options.strokeWidth, + lineJoin: options.lineJoin, + }, + }) + points.push({ + key, + markId: id, + group: datumKey, + groupLabel: datum.label, + datum, + datumIndex, + xValue: datumIndex, + yValue: datum.value, + x: geometry.center[0], + y: geometry.center[1], + color: datum.color, + }) + }) + return { + nodes: [ + { + kind: 'group', + key: id, + className: 'ts-chart__area ts-chart__geometry-morph', + children: nodes, + }, + ], + points, + } + }, + } + }, options.motion) +} + +function geometryForMode( + data: readonly MorphDatum[], + mode: MorphMode, + chart: { x: number; y: number; width: number; height: number }, +) { + const padding = Math.max(14, Math.min(chart.width, chart.height) * 0.05) + const center: Point = [chart.x + chart.width / 2, chart.y + chart.height / 2] + const radius = Math.max(12, Math.min(chart.width, chart.height) / 2 - padding) + const maximum = Math.max(...data.map((datum) => datum.value)) + + if (mode === 'bars') { + const gap = Math.max(4, chart.width * 0.012) + const width = (chart.width - padding * 2) / data.length + return data.map((datum, index) => { + const height = ((chart.height - padding * 2) * datum.value) / 100 + const x = chart.x + padding + index * width + gap / 2 + const y = chart.y + chart.height - padding - height + const barWidth = width - gap + return { + points: sampleRectangle(x, y, barWidth, height, sampleCount), + center: [x + barWidth / 2, y + height / 2] as Point, + } + }) + } + + if (mode === 'bubbles') { + const columns = 3 + const rows = 2 + const cellWidth = (chart.width - padding * 2) / columns + const cellHeight = (chart.height - padding * 2) / rows + return data.map((datum, index) => { + const column = index % columns + const row = Math.floor(index / columns) + const bubbleCenter: Point = [ + chart.x + padding + cellWidth * (column + 0.5), + chart.y + padding + cellHeight * (row + 0.5), + ] + const bubbleRadius = + Math.min(cellWidth, cellHeight) * 0.4 * Math.sqrt(datum.value / maximum) + return { + points: sampleCircle(bubbleCenter, bubbleRadius, sampleCount), + center: bubbleCenter, + } + }) + } + + const gap = 0.035 + let angle = -Math.PI / 2 + const total = data.reduce((sum, datum) => sum + datum.value, 0) + return data.map((datum, index) => { + const span = + mode === 'rose' + ? (Math.PI * 2) / data.length + : (Math.PI * 2 * datum.value) / total + const start = mode === 'rose' ? -Math.PI / 2 + index * span : angle + const end = start + span + angle = end + const outer = + mode === 'rose' ? radius * (0.38 + (0.6 * datum.value) / maximum) : radius + const inner = mode === 'rose' ? radius * 0.09 : radius * 0.55 + const points = sampleSector( + center, + inner, + outer, + start + gap, + end - gap, + sampleCount, + ) + const middleAngle = (start + end) / 2 + const middleRadius = (inner + outer) / 2 + return { + points, + center: [ + center[0] + Math.cos(middleAngle) * middleRadius, + center[1] + Math.sin(middleAngle) * middleRadius, + ] as Point, + } + }) +} + +function sampleRectangle( + x: number, + y: number, + width: number, + height: number, + count: number, +): Point[] { + const perimeter = 2 * (width + height) + return Array.from({ length: count }, (_value, index) => { + let distance = (index / count) * perimeter + if (distance < width) return [x + distance, y] as const + distance -= width + if (distance < height) return [x + width, y + distance] as const + distance -= height + if (distance < width) return [x + width - distance, y + height] as const + return [x, y + height - (distance - width)] as const + }) +} + +function sampleCircle(center: Point, radius: number, count: number): Point[] { + return Array.from({ length: count }, (_value, index) => { + const angle = -Math.PI / 2 + (index / count) * Math.PI * 2 + return [ + center[0] + Math.cos(angle) * radius, + center[1] + Math.sin(angle) * radius, + ] as const + }) +} + +function sampleSector( + center: Point, + inner: number, + outer: number, + start: number, + end: number, + count: number, +): Point[] { + const outerCount = count / 2 + const innerCount = count - outerCount + const points: Point[] = [] + for (let index = 0; index < outerCount; index += 1) { + const angle = start + ((end - start) * index) / (outerCount - 1) + points.push([ + center[0] + Math.cos(angle) * outer, + center[1] + Math.sin(angle) * outer, + ]) + } + for (let index = 0; index < innerCount; index += 1) { + const angle = end - ((end - start) * index) / (innerCount - 1) + points.push([ + center[0] + Math.cos(angle) * inner, + center[1] + Math.sin(angle) * inner, + ]) + } + return points +} + +export function modeForRevision(revision: number) { + return morphModes[Math.abs(revision) % morphModes.length] ?? 'bars' +} + +export function modeLabel(mode: MorphMode) { + return mode[0]!.toUpperCase() + mode.slice(1) +} + +export default function GeometryMorphExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '116-geometry-morph' + const viewRef = useRef(null) + + const nextRef = useRef(null) + + const interruptRef = useRef(null) + + const replayRef = useRef(null) + + const timerRef = useRef(undefined) + + const [mode, setMode] = useState(() => + modeForRevision(input.revision), + ) + + const [replayCount, setReplayCount] = useState(1) + + const [, setInterruptionCount] = useState(0) + + const [announcement, setAnnouncement] = useState('') + + const renderer = useMemo( + () => motion(), + [replayCount], + ) + + const definition = useMemo( + () => geometryMorphDefinition(morphData, mode), + [mode], + ) + + const clearTimer = () => { + if (timerRef.current !== undefined) window.clearTimeout(timerRef.current) + timerRef.current = undefined + } + + const selectMode = (nextMode: MorphMode) => { + clearTimer() + setMode(nextMode) + setAnnouncement('') + } + + const advance = () => { + const index = morphModes.indexOf(mode) + selectMode(morphModes[(index + 1) % morphModes.length] ?? 'bars') + } + + const interrupt = () => { + clearTimer() + setMode('rose') + setAnnouncement('Rose → bubbles in 180 ms') + timerRef.current = window.setTimeout(() => { + setMode('bubbles') + setInterruptionCount((value) => value + 1) + setAnnouncement('') + timerRef.current = undefined + }, 180) + } + + const replay = () => { + clearTimer() + setMode('bars') + setReplayCount((value) => value + 1) + setAnnouncement('') + } + + useEffect(() => { + clearTimer() + setMode(modeForRevision(input.revision)) + setAnnouncement('') + }, [input.revision]) + + useEffect(() => () => clearTimer(), []) + + return ( +
+ + {morphModes.map((candidate) => ( + selectMode(candidate)} + > + {modeLabel(candidate)} + + ))} + + Next + + + Interrupt + + + Replay + + + {announcement || modeLabel(mode)} + + + +
+ ) +} diff --git a/benchmarks/conformance/cases/116-geometry-morph/tanstack.test.ts b/benchmarks/conformance/cases/116-geometry-morph/tanstack.test.ts index 7f40ce6d..7418439b 100644 --- a/benchmarks/conformance/cases/116-geometry-morph/tanstack.test.ts +++ b/benchmarks/conformance/cases/116-geometry-morph/tanstack.test.ts @@ -171,7 +171,7 @@ describe('normalized-topology geometry morph', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/116-geometry-morph/tanstack.ts', + 'benchmarks/conformance/cases/116-geometry-morph/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/116-geometry-morph/tanstack.ts b/benchmarks/conformance/cases/116-geometry-morph/tanstack.ts index 47725da3..cd247921 100644 --- a/benchmarks/conformance/cases/116-geometry-morph/tanstack.ts +++ b/benchmarks/conformance/cases/116-geometry-morph/tanstack.ts @@ -1,289 +1,13 @@ -import { createMark, defineChart } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { morphData, morphModes } from './model' +import { geometryMorphDefinition, modeForRevision, modeLabel } from './example' +import { morphData } from './model' import { tanstackCase } from '../../shared/mount' -import type { - ChartMotionDefinition, - ChartPoint, - SceneNode, -} from '@tanstack/charts' -import type { MorphDatum, MorphMode } from './model' - -type Point = readonly [number, number] - -const sampleCount = 48 +export { geometryMorphDefinition, modeForRevision, modeLabel } from './example' +export { default as Example } from './example' export { mount } from './view' -export function geometryMorphDefinition( - data: readonly MorphDatum[], - mode: MorphMode, -) { - return defineChart({ - motion: { - transition: { - type: 'spring', - stiffness: 105, - damping: 16, - mass: 0.9, - }, - }, - marks: [ - normalizedTopologyMark(data, mode, { - id: 'geometry-morph', - key: (datum) => datum.id, - fill: (datum) => datum.color, - fillOpacity: 0.9, - stroke: 'Canvas', - strokeOpacity: 0.75, - strokeWidth: 1.5, - lineJoin: 'round', - motion(context) { - return { - delay: context.phase === 'enter' ? context.datumIndex * 38 : 0, - transition: - context.datum?.id === 'violet' - ? { type: 'spring', mass: 1.35 } - : undefined, - } - }, - }), - ], - x: { scale: scaleLinear().domain([0, data.length - 1]) }, - y: { scale: scaleLinear().domain([0, 100]) }, - guides: false, - margin: 0, - }) -} - export const catalogCase = tanstackCase( (input) => geometryMorphDefinition(morphData, modeForRevision(input.revision)), 'Data morphing between bars, line, area, rose, and bubbles', ) - -interface NormalizedTopologyMarkOptions { - id: string - key: (datum: MorphDatum, index: number) => string - fill: (datum: MorphDatum, index: number) => string - fillOpacity: number - stroke: string - strokeOpacity: number - strokeWidth: number - lineJoin: 'miter' | 'round' | 'bevel' - motion?: ChartMotionDefinition -} - -function normalizedTopologyMark( - data: readonly MorphDatum[], - mode: MorphMode, - options: NormalizedTopologyMarkOptions, -) { - return createMark(() => { - const id = options.id - return { - id, - channels: { - x: { scale: 'x', values: data.map((_datum, index) => index) }, - y: { - scale: 'y', - values: data.map((datum) => datum.value), - includeZero: true, - }, - }, - render: ({ chart }) => { - const geometries = geometryForMode(data, mode, chart) - const nodes: SceneNode[] = [] - const points: ChartPoint[] = [] - data.forEach((datum, datumIndex) => { - const geometry = geometries[datumIndex] - if (!geometry) return - const datumKey = options.key(datum, datumIndex) - const key = `${id}:${datumKey}` - nodes.push({ - kind: 'area', - key, - points: geometry.points, - style: { - fill: options.fill(datum, datumIndex), - fillOpacity: options.fillOpacity, - stroke: options.stroke, - strokeOpacity: options.strokeOpacity, - strokeWidth: options.strokeWidth, - lineJoin: options.lineJoin, - }, - }) - points.push({ - key, - markId: id, - group: datumKey, - groupLabel: datum.label, - datum, - datumIndex, - xValue: datumIndex, - yValue: datum.value, - x: geometry.center[0], - y: geometry.center[1], - color: datum.color, - }) - }) - return { - nodes: [ - { - kind: 'group', - key: id, - className: 'ts-chart__area ts-chart__geometry-morph', - children: nodes, - }, - ], - points, - } - }, - } - }, options.motion) -} - -function geometryForMode( - data: readonly MorphDatum[], - mode: MorphMode, - chart: { x: number; y: number; width: number; height: number }, -) { - const padding = Math.max(14, Math.min(chart.width, chart.height) * 0.05) - const center: Point = [chart.x + chart.width / 2, chart.y + chart.height / 2] - const radius = Math.max(12, Math.min(chart.width, chart.height) / 2 - padding) - const maximum = Math.max(...data.map((datum) => datum.value)) - - if (mode === 'bars') { - const gap = Math.max(4, chart.width * 0.012) - const width = (chart.width - padding * 2) / data.length - return data.map((datum, index) => { - const height = ((chart.height - padding * 2) * datum.value) / 100 - const x = chart.x + padding + index * width + gap / 2 - const y = chart.y + chart.height - padding - height - const barWidth = width - gap - return { - points: sampleRectangle(x, y, barWidth, height, sampleCount), - center: [x + barWidth / 2, y + height / 2] as Point, - } - }) - } - - if (mode === 'bubbles') { - const columns = 3 - const rows = 2 - const cellWidth = (chart.width - padding * 2) / columns - const cellHeight = (chart.height - padding * 2) / rows - return data.map((datum, index) => { - const column = index % columns - const row = Math.floor(index / columns) - const bubbleCenter: Point = [ - chart.x + padding + cellWidth * (column + 0.5), - chart.y + padding + cellHeight * (row + 0.5), - ] - const bubbleRadius = - Math.min(cellWidth, cellHeight) * 0.4 * Math.sqrt(datum.value / maximum) - return { - points: sampleCircle(bubbleCenter, bubbleRadius, sampleCount), - center: bubbleCenter, - } - }) - } - - const gap = 0.035 - let angle = -Math.PI / 2 - const total = data.reduce((sum, datum) => sum + datum.value, 0) - return data.map((datum, index) => { - const span = - mode === 'rose' - ? (Math.PI * 2) / data.length - : (Math.PI * 2 * datum.value) / total - const start = mode === 'rose' ? -Math.PI / 2 + index * span : angle - const end = start + span - angle = end - const outer = - mode === 'rose' ? radius * (0.38 + (0.6 * datum.value) / maximum) : radius - const inner = mode === 'rose' ? radius * 0.09 : radius * 0.55 - const points = sampleSector( - center, - inner, - outer, - start + gap, - end - gap, - sampleCount, - ) - const middleAngle = (start + end) / 2 - const middleRadius = (inner + outer) / 2 - return { - points, - center: [ - center[0] + Math.cos(middleAngle) * middleRadius, - center[1] + Math.sin(middleAngle) * middleRadius, - ] as Point, - } - }) -} - -function sampleRectangle( - x: number, - y: number, - width: number, - height: number, - count: number, -): Point[] { - const perimeter = 2 * (width + height) - return Array.from({ length: count }, (_value, index) => { - let distance = (index / count) * perimeter - if (distance < width) return [x + distance, y] as const - distance -= width - if (distance < height) return [x + width, y + distance] as const - distance -= height - if (distance < width) return [x + width - distance, y + height] as const - return [x, y + height - (distance - width)] as const - }) -} - -function sampleCircle(center: Point, radius: number, count: number): Point[] { - return Array.from({ length: count }, (_value, index) => { - const angle = -Math.PI / 2 + (index / count) * Math.PI * 2 - return [ - center[0] + Math.cos(angle) * radius, - center[1] + Math.sin(angle) * radius, - ] as const - }) -} - -function sampleSector( - center: Point, - inner: number, - outer: number, - start: number, - end: number, - count: number, -): Point[] { - const outerCount = count / 2 - const innerCount = count - outerCount - const points: Point[] = [] - for (let index = 0; index < outerCount; index += 1) { - const angle = start + ((end - start) * index) / (outerCount - 1) - points.push([ - center[0] + Math.cos(angle) * outer, - center[1] + Math.sin(angle) * outer, - ]) - } - for (let index = 0; index < innerCount; index += 1) { - const angle = end - ((end - start) * index) / (innerCount - 1) - points.push([ - center[0] + Math.cos(angle) * inner, - center[1] + Math.sin(angle) * inner, - ]) - } - return points -} - -export function modeForRevision(revision: number) { - return morphModes[Math.abs(revision) % morphModes.length] ?? 'bars' -} - -export function modeLabel(mode: MorphMode) { - return mode[0]!.toUpperCase() + mode.slice(1) -} diff --git a/benchmarks/conformance/cases/116-geometry-morph/view.tsx b/benchmarks/conformance/cases/116-geometry-morph/view.tsx index 7c10723c..14f64bc2 100644 --- a/benchmarks/conformance/cases/116-geometry-morph/view.tsx +++ b/benchmarks/conformance/cases/116-geometry-morph/view.tsx @@ -12,7 +12,7 @@ import { readChartMotionState, settleChartMotion } from '../../shared/motion' import { ControlBar, ControlButton } from '../../shared/react-controls' import { reactMount } from '../../shared/react-mount' import { morphData, morphModes } from './model' -import { geometryMorphDefinition, modeForRevision, modeLabel } from './tanstack' +import { geometryMorphDefinition, modeForRevision, modeLabel } from './example' import type { MorphDatum, MorphMode } from './model' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx b/benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx new file mode 100644 index 00000000..b42e59bb --- /dev/null +++ b/benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx @@ -0,0 +1,201 @@ +import { useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import type { ChartPoint, ChartScene } from '@tanstack/charts' +import type { FocusMotionRow } from './model' +import { crosshair, defineChart, dot, lineY } from '@tanstack/charts' +import { focusGroupX } from '@tanstack/charts/focus' +import { scaleBand, scaleLinear } from 'd3-scale' +import { focusMotionPeriods, focusMotionRows, focusMotionSeries } from './model' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const colors = ['#7c3aed', '#0891b2', '#ea580c'] + +const focusSpring = { + type: 'spring' as const, + stiffness: 240, + damping: 22, + mass: 0.72, +} + +export function focusCursorMotionDefinition() { + return defineChart({ + marks: [ + lineY(focusMotionRows, { + id: 'series-lines', + x: 'period', + y: 'value', + z: 'series', + color: 'series', + key: 'id', + strokeWidth: 2.5, + strokeOpacity: 0.68, + states: [ + { + when: { focus: 'unmatched' }, + style: { opacity: 0.12, strokeWidth: 1.5 }, + transition: focusSpring, + }, + { + when: { focus: 'series' }, + style: { opacity: 1, strokeOpacity: 1, strokeWidth: 4.5 }, + transition: focusSpring, + }, + ], + }), + dot(focusMotionRows, { + id: 'series-points', + x: 'period', + y: 'value', + z: 'series', + color: 'series', + key: 'id', + r: 4, + stroke: 'Canvas', + strokeWidth: 1.5, + states: [ + { + when: { focus: 'unmatched' }, + style: { opacity: 0.14, r: 2.5 }, + transition: focusSpring, + }, + { + when: { focus: 'group' }, + style: { opacity: 0.88, r: 5.5 }, + transition: focusSpring, + }, + { + when: { focus: 'primary' }, + style: { opacity: 1, r: 9, strokeWidth: 3 }, + transition: focusSpring, + }, + ], + }), + crosshair({ + id: 'focus-motion-crosshair', + stroke: 'CanvasText', + strokeOpacity: 0.48, + strokeWidth: 1, + strokeDasharray: '4 4', + x: { + label: { + format: (value) => String(value), + offset: 8, + fill: 'CanvasText', + fontSize: 10, + fontWeight: 700, + }, + }, + y: { + label: { + format: (value) => String(value), + offset: 22, + fill: 'CanvasText', + fontSize: 10, + fontWeight: 700, + }, + }, + marker: { + radius: 5, + fill: 'Canvas', + stroke: 'CanvasText', + strokeWidth: 1.5, + }, + motion: { + transition: { + type: 'spring', + stiffness: 320, + damping: 28, + mass: 0.72, + restDelta: 0.02, + restSpeed: 0.02, + }, + }, + }), + ], + x: { + scale: scaleBand().domain(focusMotionPeriods).padding(0.1), + }, + y: { scale: scaleLinear().domain([20, 90]), grid: true }, + color: { domain: focusMotionSeries, range: colors }, + focus: focusGroupX, + focusRing: false, + maxFocusDistance: Number.POSITIVE_INFINITY, + tooltip: false, + keyboard: true, + margin: { top: 24, right: 26, bottom: 38, left: 46 }, + }) +} + +export default function FocusCursorMotionExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '117-focus-cursor-motion' + const viewRef = useRef(null) + + const sceneRef = useRef>(null) + + const [focused, setFocused] = useState< + readonly ChartPoint[] + >([]) + + const renderer = useMemo( + () => motion({ initial: false }), + [], + ) + + const definition = useMemo(focusCursorMotionDefinition, []) + + const primary = focused[0] + + return ( +
+ { + sceneRef.current = context.scene + }} + /> + + {primary + ? `${primary.datum.period} · ${primary.datum.series} · ${focused.length} grouped` + : 'Hover or use ← →'} + +
+ ) +} diff --git a/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.test.ts b/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.test.ts index 92d5b1e0..c92eb1c8 100644 --- a/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.test.ts +++ b/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.test.ts @@ -192,7 +192,7 @@ describe('definition-owned focus cursor motion', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts', + 'benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts b/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts index bc7f3b49..384475d5 100644 --- a/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts +++ b/benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts @@ -1,126 +1,10 @@ -import { crosshair, defineChart, dot, lineY } from '@tanstack/charts' -import { focusGroupX } from '@tanstack/charts/focus' -import { scaleBand, scaleLinear } from 'd3-scale' -import { focusMotionPeriods, focusMotionRows, focusMotionSeries } from './model' +import { focusCursorMotionDefinition } from './example' +export { focusCursorMotionDefinition } from './example' import { tanstackCase } from '../../shared/mount' -const colors = ['#7c3aed', '#0891b2', '#ea580c'] -const focusSpring = { - type: 'spring' as const, - stiffness: 240, - damping: 22, - mass: 0.72, -} -export { mount } from './view' +export { default as Example } from './example' -export function focusCursorMotionDefinition() { - return defineChart({ - marks: [ - lineY(focusMotionRows, { - id: 'series-lines', - x: 'period', - y: 'value', - z: 'series', - color: 'series', - key: 'id', - strokeWidth: 2.5, - strokeOpacity: 0.68, - states: [ - { - when: { focus: 'unmatched' }, - style: { opacity: 0.12, strokeWidth: 1.5 }, - transition: focusSpring, - }, - { - when: { focus: 'series' }, - style: { opacity: 1, strokeOpacity: 1, strokeWidth: 4.5 }, - transition: focusSpring, - }, - ], - }), - dot(focusMotionRows, { - id: 'series-points', - x: 'period', - y: 'value', - z: 'series', - color: 'series', - key: 'id', - r: 4, - stroke: 'Canvas', - strokeWidth: 1.5, - states: [ - { - when: { focus: 'unmatched' }, - style: { opacity: 0.14, r: 2.5 }, - transition: focusSpring, - }, - { - when: { focus: 'group' }, - style: { opacity: 0.88, r: 5.5 }, - transition: focusSpring, - }, - { - when: { focus: 'primary' }, - style: { opacity: 1, r: 9, strokeWidth: 3 }, - transition: focusSpring, - }, - ], - }), - crosshair({ - id: 'focus-motion-crosshair', - stroke: 'CanvasText', - strokeOpacity: 0.48, - strokeWidth: 1, - strokeDasharray: '4 4', - x: { - label: { - format: (value) => String(value), - offset: 8, - fill: 'CanvasText', - fontSize: 10, - fontWeight: 700, - }, - }, - y: { - label: { - format: (value) => String(value), - offset: 22, - fill: 'CanvasText', - fontSize: 10, - fontWeight: 700, - }, - }, - marker: { - radius: 5, - fill: 'Canvas', - stroke: 'CanvasText', - strokeWidth: 1.5, - }, - motion: { - transition: { - type: 'spring', - stiffness: 320, - damping: 28, - mass: 0.72, - restDelta: 0.02, - restSpeed: 0.02, - }, - }, - }), - ], - x: { - scale: scaleBand().domain(focusMotionPeriods).padding(0.1), - }, - y: { scale: scaleLinear().domain([20, 90]), grid: true }, - color: { domain: focusMotionSeries, range: colors }, - focus: focusGroupX, - focusRing: false, - maxFocusDistance: Number.POSITIVE_INFINITY, - tooltip: false, - keyboard: true, - margin: { top: 24, right: 26, bottom: 38, left: 46 }, - }) -} +export { mount } from './view' export const catalogCase = tanstackCase( focusCursorMotionDefinition, diff --git a/benchmarks/conformance/cases/117-focus-cursor-motion/view.tsx b/benchmarks/conformance/cases/117-focus-cursor-motion/view.tsx index f65a7983..c9a9290b 100644 --- a/benchmarks/conformance/cases/117-focus-cursor-motion/view.tsx +++ b/benchmarks/conformance/cases/117-focus-cursor-motion/view.tsx @@ -10,7 +10,7 @@ import { Chart } from '@tanstack/charts/react/core' import { readChartMotionState } from '../../shared/motion' import { reactMount } from '../../shared/react-mount' import { focusMotionPeriods, focusMotionSeries } from './model' -import { focusCursorMotionDefinition } from './tanstack' +import { focusCursorMotionDefinition } from './example' import type { ChartPoint, ChartScene } from '@tanstack/charts' import type { FocusMotionRow } from './model' import type { ConformanceTestDriver } from '../../types' diff --git a/benchmarks/conformance/cases/118-token-usage-calendar/example.tsx b/benchmarks/conformance/cases/118-token-usage-calendar/example.tsx new file mode 100644 index 00000000..9b056d3a --- /dev/null +++ b/benchmarks/conformance/cases/118-token-usage-calendar/example.tsx @@ -0,0 +1,139 @@ +import { cell, defineChart } from '@tanstack/charts' +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { portal } from '@tanstack/charts/tooltip/portal' +import { scaleBand, scaleOrdinal } from 'd3-scale' +import { + calendarMonthTicks, + calendarWeekCount, + formatTokenUsage, + tokenUsageCalendar, + usageColors, + usageLevels, + weekdays, +} from './model' +import { + calendarBandPaddingInner, + calendarBandPaddingOuter, + calendarBottomMargin, + calendarMargin, +} from './layout' + +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +const weekDomain = Array.from( + { length: calendarWeekCount }, + (_value, index) => index, +) + +export const exampleAriaLabel = + 'Token activity from August 2025 through July 2026. Weeks are columns and Sunday through Saturday are rows. Pale gray means no usage; blue intensity ranges from up to 25 million through over 150 million tokens.' + +export const exampleTooltip = { + anchor: 'point' as const, + className: 'token-activity-tooltip', + format: ({ + datum, + }: { + datum: ReturnType[number] + }) => formatTokenUsage(datum), + offset: 5, + portal, +} + +export function createExampleChart(input: ExampleOptions) { + const days = tokenUsageCalendar(input.revision) + const monthTicks = calendarMonthTicks() + + return defineChart( + defineChart({ + marks: [ + cell(days, { + x: 'week', + y: 'weekday', + color: 'level', + key: 'dateKey', + inset: 0, + radius: 3, + }), + ], + x: { + scale: scaleBand() + .domain(weekDomain) + .paddingInner(calendarBandPaddingInner) + .paddingOuter(calendarBandPaddingOuter), + axis: { + line: false, + ticks: { + values: monthTicks.values, + size: 0, + padding: input.preview ? 3 : 7, + format: (week: number) => monthTicks.labels.get(week) ?? '', + }, + tickLabels: { + fontSize: input.preview ? 8 : 13, + opacity: 0.62, + anchor: ({ index }) => (index === 0 ? 'start' : undefined), + dx: ({ index, bandwidth }) => + index === 0 ? -bandwidth / 2 : undefined, + thin: { minGap: input.preview ? 2 : 8, priority: 'ends' }, + }, + }, + }, + y: { + scale: scaleBand() + .domain(weekdays) + .paddingInner(calendarBandPaddingInner) + .paddingOuter(calendarBandPaddingOuter), + axis: false, + }, + color: { + scale: scaleOrdinal() + .domain(usageLevels) + .range(usageColors), + }, + margin: input.preview + ? { top: 0, right: 0, bottom: 16, left: 0 } + : { + ...calendarMargin, + bottom: calendarBottomMargin(input.width, input.height), + }, + }), + { + keyboard: true, + tooltip: { use: tooltip, ...exampleTooltip }, + }, + ) +} + +export const tokenUsageCalendarDefinition = createExampleChart + +export const chart = createExampleChart({ + width: 720, + height: 300, + revision: 0, +}) + +export default function Example() { + return ( +
+ + +
+ ) +} diff --git a/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.test.ts b/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.test.ts index b4124c67..1ee23fdc 100644 --- a/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.test.ts +++ b/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.test.ts @@ -103,7 +103,7 @@ describe('definition-owned token activity calendar', () => { const definitionSource = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts', + 'benchmarks/conformance/cases/118-token-usage-calendar/example.tsx', ), 'utf8', ) @@ -115,7 +115,7 @@ describe('definition-owned token activity calendar', () => { 'utf8', ) - expect(definitionSource).toContain('fontSize: 13') + expect(definitionSource).toContain('fontSize: input.preview ? 8 : 13') expect(definitionSource).toContain('opacity: 0.62') expect(definitionSource).toContain("index === 0 ? 'start' : undefined") expect(definitionSource).toContain('-bandwidth / 2') diff --git a/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts b/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts index 5ef8cd49..a45cc810 100644 --- a/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts +++ b/benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts @@ -1,115 +1,13 @@ -import { cell, defineChart } from '@tanstack/charts' -import { portal } from '@tanstack/charts/tooltip/portal' -import { scaleBand, scaleOrdinal } from 'd3-scale' -import { - calendarMonthTicks, - calendarWeekCount, - formatTokenUsage, - tokenUsageCalendar, - usageColors, - usageLevels, - weekdays, -} from './model' -import { - calendarBandPaddingInner, - calendarBandPaddingOuter, - calendarBottomMargin, - calendarMargin, -} from './layout' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' import { withTokenActivityShell } from './shell' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' -const weekDomain = Array.from( - { length: calendarWeekCount }, - (_value, index) => index, -) - -export const tokenUsageCalendarDefinition = (input: ConformanceInput) => { - const days = tokenUsageCalendar(input.revision) - const monthTicks = calendarMonthTicks() - - return defineChart({ - marks: [ - cell(days, { - x: 'week', - y: 'weekday', - color: 'level', - key: 'dateKey', - inset: 0, - radius: 3, - }), - ], - x: { - scale: scaleBand() - .domain(weekDomain) - .paddingInner(calendarBandPaddingInner) - .paddingOuter(calendarBandPaddingOuter), - axis: { - line: false, - ticks: { - values: monthTicks.values, - size: 0, - padding: input.preview === true ? 3 : 7, - format: (week: number) => monthTicks.labels.get(week) ?? '', - }, - tickLabels: { - fontSize: 13, - ...(input.preview === true ? { fontSize: 8 } : {}), - opacity: 0.62, - anchor: ({ index }) => (index === 0 ? 'start' : undefined), - dx: ({ index, bandwidth }) => - index === 0 ? -bandwidth / 2 : undefined, - thin: { - minGap: input.preview === true ? 2 : 8, - priority: 'ends', - }, - }, - }, - }, - y: { - scale: scaleBand() - .domain(weekdays) - .paddingInner(calendarBandPaddingInner) - .paddingOuter(calendarBandPaddingOuter), - axis: false, - }, - color: { - scale: scaleOrdinal() - .domain(usageLevels) - .range(usageColors), - }, - margin: - input.preview === true - ? { top: 0, right: 0, bottom: 16, left: 0 } - : { - ...calendarMargin, - bottom: calendarBottomMargin(input.width, input.height), - }, - }) -} +export * from './example' -const ariaLabel = - 'Token activity from August 2025 through July 2026. Weeks are columns and Sunday through Saturday are rows. Pale gray means no usage; blue intensity ranges from up to 25 million through over 150 million tokens.' -const interactiveTooltip = { - anchor: 'point' as const, - className: 'token-activity-tooltip', - format: ({ - datum, - }: { - datum: ReturnType[number] - }) => formatTokenUsage(datum), - offset: 5, - portal, -} - -export const catalogCase = tanstackCase( - tokenUsageCalendarDefinition, - ariaLabel, - interactiveTooltip, +export const catalogCase = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { guides: true, margin: true }, ) -export const mount = withTokenActivityShell( - tanstackMount(tokenUsageCalendarDefinition, ariaLabel, interactiveTooltip), -) +export const mount = withTokenActivityShell(catalogCase) diff --git a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/chart.ts b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/example.tsx similarity index 62% rename from benchmarks/conformance/cases/119-stacked-bar-band-cursor/chart.ts rename to benchmarks/conformance/cases/119-stacked-bar-band-cursor/example.tsx index 1dfa54c1..35685a77 100644 --- a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/chart.ts +++ b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/example.tsx @@ -1,10 +1,8 @@ -import { - barY, - colorLegend, - crosshair, - defineChart, - stack, -} from '@tanstack/charts' +import { useMemo } from 'react' +import { defineChart } from '@tanstack/charts' +import { Chart } from '@tanstack/charts/react/core' +import { stackedCursorRowsForRevision } from './model' +import { barY, colorLegend, crosshair, stack } from '@tanstack/charts' import { motion } from '@tanstack/charts/motion' import { scaleBand, scaleLinear } from 'd3-scale' import { @@ -17,6 +15,11 @@ import { stackedCursorPeriods, } from './model' import type { StackedCursorRow } from './model' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} const cursorTransition = { type: 'spring' as const, @@ -112,3 +115,49 @@ export const stackedCursorDefinition = (rows: readonly StackedCursorRow[]) => tooltip: false, keyboard: true, }) + +export default function StackedCursorCatalogView({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '119-stacked-bar-band-cursor' + const renderer = useMemo(createStackedCursorRenderer, []) + + const definition = useMemo( + () => + defineChart( + stackedCursorDefinition(stackedCursorRowsForRevision(input.revision)), + { + svgAnimation: false, + ...(true ? {} : { focus: false }), + keyboard: true, + tooltip: false, + }, + ), + [true, input.revision], + ) + + const ariaLabel = 'Crimean War deaths with x band and y rule cursors' + + const ariaDescription = + 'Move over a stacked bar. The x cursor highlights the full stack and the dotted y cursor marks the focused segment endpoint.' + + return ( +
+ +
+ ) +} diff --git a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts index 048c57c5..74013f25 100644 --- a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts +++ b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts @@ -1,5 +1,5 @@ import { mountChartRenderer } from '@tanstack/charts/renderer' -import { createStackedCursorRenderer, stackedCursorDefinition } from './chart' +import { createStackedCursorRenderer, stackedCursorDefinition } from './example' import { catalogPreviewDefinition } from '../../shared/preview' import { tanstackCase } from '../../shared/mount' import { @@ -14,6 +14,8 @@ import type { ConformanceTestDriver, } from '../../types' +export { default as Example } from './example' + export const catalogCase = tanstackCase( (input) => stackedCursorDefinition(stackedCursorRowsForRevision(input.revision)), diff --git a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/view.tsx b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/view.tsx index 0d1f77ab..86b6730c 100644 --- a/benchmarks/conformance/cases/119-stacked-bar-band-cursor/view.tsx +++ b/benchmarks/conformance/cases/119-stacked-bar-band-cursor/view.tsx @@ -1,7 +1,7 @@ import { forwardRef, useMemo } from 'react' import { defineChart } from '@tanstack/charts' import { Chart } from '@tanstack/charts/react/core' -import { createStackedCursorRenderer, stackedCursorDefinition } from './chart' +import { createStackedCursorRenderer, stackedCursorDefinition } from './example' import { stackedCursorRowsForRevision } from './model' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/120-themed-interactive-area/chart.ts b/benchmarks/conformance/cases/120-themed-interactive-area/example.tsx similarity index 61% rename from benchmarks/conformance/cases/120-themed-interactive-area/chart.ts rename to benchmarks/conformance/cases/120-themed-interactive-area/example.tsx index efc555c3..e1e7670c 100644 --- a/benchmarks/conformance/cases/120-themed-interactive-area/chart.ts +++ b/benchmarks/conformance/cases/120-themed-interactive-area/example.tsx @@ -1,18 +1,116 @@ +import { useMemo, useState } from 'react' +import { defineChart } from '@tanstack/charts' +import { motion } from '@tanstack/charts/motion' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' import { - areaY, - crosshair, - d3Curve, - defineChart, - dot, - lineY, -} from '@tanstack/charts' + formatThemedAreaTooltip, + themedAreaRanges, + themedAreaRangeDays, + themedAreaRows, +} from './model' +import type { ChartTooltipOptions } from '@tanstack/charts' +import type { ThemedAreaRange, ThemedAreaRow } from './model' +import './styles.css' +import { areaY, crosshair, d3Curve, dot, lineY } from '@tanstack/charts' import { focusGroupX } from '@tanstack/charts/focus' import { decorative } from '@tanstack/charts/mark/decorative' import { scaleLinear, scaleUtc } from 'd3-scale' import { curveMonotoneX } from 'd3-shape' import { formatThemedAreaTick } from './model' import type { ChartMotionSpringTransition } from '@tanstack/charts' -import type { ThemedAreaRow } from './model' +export const exampleAriaLabel = + 'Daily visitors with selectable 7, 30, and 90 day ranges' + +export const themedAreaTooltip: ChartTooltipOptions = { + anchor: 'point', + placement: ['top', 'right', 'left', 'bottom'], + className: 'themed-area-tooltip', + offset: 10, + format: ({ datum }) => formatThemedAreaTooltip(datum), +} + +export function createExampleChart( + range: ThemedAreaRange, + revision: number, + width: number, + height: number, +) { + return defineChart( + themedInteractiveAreaDefinition(themedAreaRows(range, revision), { + width, + height, + }), + { + keyboard: true, + tooltip: { use: tooltip, ...themedAreaTooltip }, + }, + ) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function Example({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const [range, setRange] = useState('30d') + const definition = useMemo( + () => createExampleChart(range, revision, width, height - 70), + [height, range, revision, width], + ) + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: themedAreaSpring, + respectReducedMotion: true, + }), + [], + ) + + return ( +
+
+
+

Traffic

+

Daily visitors

+
+
+ {themedAreaRanges.map((value) => ( + + ))} +
+
+ +
+ ) +} export interface ThemedAreaChartInput { width: number @@ -30,10 +128,15 @@ export const themedAreaSpring = { } satisfies ChartMotionSpringTransition const smooth = d3Curve(curveMonotoneX) + const accent = 'var(--themed-area-accent, #3b82f6)' + const surface = 'var(--themed-area-surface, Canvas)' + const foreground = 'var(--themed-area-foreground, CanvasText)' + const muted = 'var(--themed-area-muted, GrayText)' + const grid = 'var(--themed-area-grid, CanvasText)' export function themedInteractiveAreaDefinition( diff --git a/benchmarks/conformance/cases/120-themed-interactive-area/styles.css b/benchmarks/conformance/cases/120-themed-interactive-area/styles.css new file mode 100644 index 00000000..77058165 --- /dev/null +++ b/benchmarks/conformance/cases/120-themed-interactive-area/styles.css @@ -0,0 +1,78 @@ +.themed-area-card { + --themed-area-surface: light-dark(#ffffff, #09090b); + --themed-area-foreground: light-dark(#18181b, #fafafa); + --themed-area-muted: light-dark(#71717a, #a1a1aa); + --themed-area-grid: light-dark(#64748b, #94a3b8); + --themed-area-border: light-dark( + rgba(15, 23, 42, 0.11), + rgba(255, 255, 255, 0.1) + ); + --themed-area-accent: light-dark(#2563eb, #60a5fa); + --ts-chart-tooltip-background: color-mix( + in srgb, + var(--themed-area-surface) 94%, + transparent + ); + --ts-chart-tooltip-color: var(--themed-area-foreground); + --ts-chart-tooltip-border: 1px solid var(--themed-area-border); + --ts-chart-tooltip-border-radius: 8px; + --ts-chart-tooltip-shadow: 0 8px 24px rgba(15, 23, 42, 0.13); + box-sizing: border-box; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + overflow: hidden; + color: var(--themed-area-foreground); + background: var(--themed-area-surface); + border: 1px solid var(--themed-area-border); + border-radius: 16px; + font-family: ui-sans-serif, system-ui, sans-serif; +} + +.themed-area-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 20px 8px; +} + +.themed-area-card__title { + margin: 0; + font-size: 14px; + line-height: 1.25; +} + +.themed-area-card__description { + margin: 4px 0 0; + color: var(--themed-area-muted); + font-size: 11px; +} + +.themed-area-card__ranges { + display: inline-flex; + gap: 2px; + padding: 3px; + border: 1px solid var(--themed-area-border); + border-radius: 9px; +} + +.themed-area-card__range { + min-width: 38px; + min-height: 30px; + padding: 0 9px; + color: var(--themed-area-muted); + background: transparent; + border: 0; + border-radius: 6px; + cursor: pointer; + font: + 600 10px/1 ui-sans-serif, + system-ui, + sans-serif; +} + +.themed-area-card__range[aria-pressed='true'] { + color: var(--themed-area-foreground); + background: var(--themed-area-surface); + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.11); +} diff --git a/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.test.ts b/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.test.ts index 5aa7a72b..4e4cc1f4 100644 --- a/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.test.ts +++ b/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.test.ts @@ -1,6 +1,6 @@ import { createChartRuntime } from '@tanstack/charts' import { describe, expect, it } from 'vitest' -import { themedInteractiveAreaDefinition } from './chart' +import { themedInteractiveAreaDefinition } from './example' import { themedAreaRows } from './model' import { catalogCase, mount } from './tanstack' import type { ConformanceInput } from '../../types' diff --git a/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.ts b/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.ts index 338916f7..6f8073d6 100644 --- a/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.ts +++ b/benchmarks/conformance/cases/120-themed-interactive-area/tanstack.ts @@ -1,17 +1,14 @@ +import { themedAreaTooltip } from './example' +export { themedAreaTooltip } from './example' import { defineChart } from '@tanstack/charts' import { motion } from '@tanstack/charts/motion' import { mountChartRenderer } from '@tanstack/charts/renderer' import { tooltip } from '@tanstack/charts/tooltip' import { readChartMotionState, settleChartMotion } from '../../shared/motion' import { tanstackCase } from '../../shared/mount' -import { themedAreaSpring, themedInteractiveAreaDefinition } from './chart' -import { - formatThemedAreaTooltip, - themedAreaRanges, - themedAreaRows, - themedAreaRangeDays, -} from './model' -import type { ChartPoint, ChartTooltipOptions } from '@tanstack/charts' +import { themedAreaSpring, themedInteractiveAreaDefinition } from './example' +import { themedAreaRanges, themedAreaRows, themedAreaRangeDays } from './model' +import type { ChartPoint } from '@tanstack/charts' import type { ThemedAreaRange, ThemedAreaRow } from './model' import type { ConformanceInput, @@ -19,17 +16,11 @@ import type { ConformanceTestDriver, } from '../../types' +export { default as Example } from './example' + const ariaLabel = 'Daily visitors with selectable 7, 30, and 90 day ranges' let themedAreaMountId = 0 -export const themedAreaTooltip: ChartTooltipOptions = { - anchor: 'point', - placement: ['top', 'right', 'left', 'bottom'], - className: 'themed-area-tooltip', - offset: 10, - format: ({ datum }) => formatThemedAreaTooltip(datum), -} - export const catalogCase = tanstackCase( (input) => themedInteractiveAreaDefinition(themedAreaRows('30d', input.revision), { diff --git a/benchmarks/conformance/cases/121-active-bar-dashboard/chart.ts b/benchmarks/conformance/cases/121-active-bar-dashboard/chart.ts deleted file mode 100644 index 4049002c..00000000 --- a/benchmarks/conformance/cases/121-active-bar-dashboard/chart.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { barY, crosshair, defineChart } from '@tanstack/charts' -import { scaleBand } from '@tanstack/charts/scales/band' -import { scaleLinear } from '@tanstack/charts/scales/linear' -import { tooltip } from '@tanstack/charts/tooltip' -import { dashboardRows } from './model' -import type { ChartTooltipOptions } from '@tanstack/charts' -import type { ConformanceInput } from '../../types' -import type { DashboardMetric, DashboardRow } from './model' - -export const dashboardSpring = { - type: 'spring' as const, - stiffness: 210, - damping: 24, - mass: 0.78, -} - -const tickIds = ['01', '06', '12', '18', '24'] - -const tooltipOptions: ChartTooltipOptions = { - anchor: 'point', - placement: ['top', 'right', 'left'], - className: 'active-bar-tooltip', -} - -export function activeBarDashboardDefinition( - input: ConformanceInput, - metric: DashboardMetric, -) { - const rows = dashboardRows(input.revision) - const maximum = Math.max(...rows.map((row) => row[metric])) - - return defineChart( - { - marks: [ - barY(rows, { - id: 'daily-visitors', - x: 'id', - y: metric, - key: 'id', - fill: 'url(#visitor-bars)', - radius: 4, - inset: input.preview ? 1.5 : 2.5, - states: [ - { - when: { focus: 'unmatched' }, - style: { opacity: 0.26 }, - transition: dashboardSpring, - }, - { - when: { focus: 'primary' }, - style: { opacity: 1, inset: input.preview ? 0.5 : 1.5 }, - transition: dashboardSpring, - }, - ], - }), - crosshair({ - id: 'active-bar-ring', - x: { - band: { - inset: -2, - radius: 6, - fill: 'transparent', - stroke: 'var(--ts-chart-1)', - strokeOpacity: 0.92, - strokeWidth: 1.5, - }, - }, - y: false, - motion: { transition: dashboardSpring }, - }), - ], - x: { - scale: scaleBand() - .domain(rows.map((row) => row.id)) - .paddingInner(0.18) - .paddingOuter(0.08), - axis: { - line: false, - ticks: { - values: tickIds, - size: 0, - padding: input.preview ? 4 : 8, - format: (value) => rows.find((row) => row.id === value)?.day ?? '', - }, - tickLabels: { - fontSize: input.preview ? 8 : 10, - opacity: 0.52, - }, - }, - }, - y: { - scale: scaleLinear().domain([0, Math.ceil(maximum * 1.12)]), - grid: true, - axis: false, - }, - gradients: [ - { - id: 'visitor-bars', - x1: 0, - y1: 1, - x2: 0, - y2: 0, - stops: [ - { offset: 0, color: 'var(--ts-chart-1)', opacity: 0.42 }, - { offset: 1, color: 'var(--ts-chart-1)', opacity: 0.96 }, - ], - }, - ], - motion: { transition: dashboardSpring }, - margin: input.preview - ? { top: 8, right: 18, bottom: 22, left: 18 } - : { top: 12, right: 8, bottom: 34, left: 8 }, - }, - { - focus: 'nearest', - keyboard: input.interactive, - tooltip: input.interactive - ? { - use: tooltip, - ...tooltipOptions, - format: ({ datum }) => - `${datum.day} · ${datum[metric].toLocaleString('en-US')} ${metric} visitors`, - } - : false, - }, - ) -} diff --git a/benchmarks/conformance/cases/121-active-bar-dashboard/example.tsx b/benchmarks/conformance/cases/121-active-bar-dashboard/example.tsx new file mode 100644 index 00000000..6e0fba8a --- /dev/null +++ b/benchmarks/conformance/cases/121-active-bar-dashboard/example.tsx @@ -0,0 +1,299 @@ +import { useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { dashboardRows, metricTotal } from './model' +import type { CSSProperties } from 'react' +import type { DashboardMetric, DashboardRow } from './model' +import { barY, crosshair, defineChart } from '@tanstack/charts' +import { scaleBand } from '@tanstack/charts/scales/band' +import { scaleLinear } from '@tanstack/charts/scales/linear' +import { tooltip } from '@tanstack/charts/tooltip' +import type { ChartTooltipOptions } from '@tanstack/charts' +const metricLabels: Record = { + desktop: 'Desktop', + mobile: 'Mobile', +} + +const cardStyle: CSSProperties & Record<`--${string}`, string> = { + '--ts-chart-1': '#7c3aed', + '--ts-chart-tooltip-background': + 'color-mix(in srgb, Canvas 94%, transparent)', + '--ts-chart-tooltip-color': 'CanvasText', + '--ts-chart-tooltip-border': + '1px solid color-mix(in srgb, CanvasText 12%, transparent)', + '--ts-chart-tooltip-border-radius': '10px', + '--ts-chart-tooltip-shadow': + '0 12px 34px color-mix(in srgb, CanvasText 14%, transparent)', + '--ts-chart-tooltip-font': + '600 12px/1.35 Inter, ui-sans-serif, system-ui, sans-serif', + boxSizing: 'border-box', + width: '100%', + height: '100%', + overflow: 'hidden', + border: '1px solid color-mix(in srgb, currentColor 12%, transparent)', + borderRadius: 18, + color: 'CanvasText', + background: 'Canvas', + fontFamily: + "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + boxShadow: '0 18px 55px color-mix(in srgb, CanvasText 7%, transparent)', +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export const dashboardSpring = { + type: 'spring' as const, + stiffness: 210, + damping: 24, + mass: 0.78, +} + +const tickIds = ['01', '06', '12', '18', '24'] + +const tooltipOptions: ChartTooltipOptions = { + anchor: 'point', + placement: ['top', 'right', 'left'], + className: 'active-bar-tooltip', +} + +export function activeBarDashboardDefinition( + input: ExampleChartInput, + metric: DashboardMetric, +) { + const rows = dashboardRows(input.revision) + const maximum = Math.max(...rows.map((row) => row[metric])) + + return defineChart( + { + marks: [ + barY(rows, { + id: 'daily-visitors', + x: 'id', + y: metric, + key: 'id', + fill: 'url(#visitor-bars)', + radius: 4, + inset: input.preview ? 1.5 : 2.5, + states: [ + { + when: { focus: 'unmatched' }, + style: { opacity: 0.26 }, + transition: dashboardSpring, + }, + { + when: { focus: 'primary' }, + style: { opacity: 1, inset: input.preview ? 0.5 : 1.5 }, + transition: dashboardSpring, + }, + ], + }), + crosshair({ + id: 'active-bar-ring', + x: { + band: { + inset: -2, + radius: 6, + fill: 'transparent', + stroke: 'var(--ts-chart-1)', + strokeOpacity: 0.92, + strokeWidth: 1.5, + }, + }, + y: false, + motion: { transition: dashboardSpring }, + }), + ], + x: { + scale: scaleBand() + .domain(rows.map((row) => row.id)) + .paddingInner(0.18) + .paddingOuter(0.08), + axis: { + line: false, + ticks: { + values: tickIds, + size: 0, + padding: input.preview ? 4 : 8, + format: (value) => rows.find((row) => row.id === value)?.day ?? '', + }, + tickLabels: { + fontSize: input.preview ? 8 : 10, + opacity: 0.52, + }, + }, + }, + y: { + scale: scaleLinear().domain([0, Math.ceil(maximum * 1.12)]), + grid: true, + axis: false, + }, + gradients: [ + { + id: 'visitor-bars', + x1: 0, + y1: 1, + x2: 0, + y2: 0, + stops: [ + { offset: 0, color: 'var(--ts-chart-1)', opacity: 0.42 }, + { offset: 1, color: 'var(--ts-chart-1)', opacity: 0.96 }, + ], + }, + ], + motion: { transition: dashboardSpring }, + margin: input.preview + ? { top: 8, right: 18, bottom: 22, left: 18 } + : { top: 12, right: 8, bottom: 34, left: 8 }, + }, + { + focus: 'nearest', + keyboard: input.interactive, + tooltip: input.interactive + ? { + use: tooltip, + ...tooltipOptions, + format: ({ datum }) => + `${datum.day} · ${datum[metric].toLocaleString('en-US')} ${metric} visitors`, + } + : false, + }, + ) +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export default function ActiveBarDashboard({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '121-active-bar-dashboard' + const [metric, setMetric] = useState('desktop') + + const root = useRef(null) + + const chartHost = useRef(null) + + const desktopButton = useRef(null) + + const mobileButton = useRef(null) + + const focusedId = useRef(null) + + const renderer = useMemo( + () => motion({ initial: !false }), + [false], + ) + + const rows = dashboardRows(input.revision) + + const definition = useMemo( + () => activeBarDashboardDefinition(input, metric), + [input, metric], + ) + + const chartWidth = Math.max(180, input.width - 32) + + const chartHeight = Math.max(150, input.height - 132) + + return ( +
+
+
+
+ Total visitors +
+
+ {metricTotal(rows, metric).toLocaleString('en-US')} +
+
+
+ {(['desktop', 'mobile'] as const).map((id) => ( + + ))} +
+
+
+ + idPrefix={idPrefix} + definition={definition} + renderer={renderer} + width={chartWidth} + height={chartHeight} + ariaLabel={`Daily ${metricLabels[metric].toLowerCase()} visitors`} + onRender={({ container }) => { + chartHost.current = container + }} + onFocusChange={(point) => { + focusedId.current = point?.datum.id ?? null + }} + /> +
+
+ ) +} diff --git a/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.test.ts b/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.test.ts index 5925adca..39185d0a 100644 --- a/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.test.ts +++ b/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { createChartScene } from '@tanstack/charts' -import { activeBarDashboardDefinition } from './chart' +import { activeBarDashboardDefinition } from './example' import { dashboardRows, metricTotal } from './model' import { mount } from './view' diff --git a/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.ts b/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.ts index 81515ee0..5d846941 100644 --- a/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.ts +++ b/benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.ts @@ -1,2 +1,3 @@ -export { activeBarDashboardDefinition } from './chart' +export { default as Example } from './example' +export { activeBarDashboardDefinition } from './example' export { mount } from './view' diff --git a/benchmarks/conformance/cases/121-active-bar-dashboard/view.tsx b/benchmarks/conformance/cases/121-active-bar-dashboard/view.tsx index 2eab63d7..b8f84287 100644 --- a/benchmarks/conformance/cases/121-active-bar-dashboard/view.tsx +++ b/benchmarks/conformance/cases/121-active-bar-dashboard/view.tsx @@ -8,7 +8,7 @@ import { import { motion } from '@tanstack/charts/motion' import { Chart } from '@tanstack/charts/react/core' import { settleChartMotion } from '../../shared/motion' -import { activeBarDashboardDefinition } from './chart' +import { activeBarDashboardDefinition } from './example' import { dashboardRows, metricTotal } from './model' import { reactMount } from '../../shared/react-mount' import type { CSSProperties } from 'react' diff --git a/benchmarks/conformance/cases/122-premium-kpi-sparklines/chart.ts b/benchmarks/conformance/cases/122-premium-kpi-sparklines/chart.ts deleted file mode 100644 index 65678e72..00000000 --- a/benchmarks/conformance/cases/122-premium-kpi-sparklines/chart.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { areaY, d3Curve, defineChart, lineY } from '@tanstack/charts' -import { motion } from '@tanstack/charts/motion' -import { scaleLinear } from '@tanstack/charts/scales/linear' -import { scalePoint } from '@tanstack/charts/scales/point' -import { curveMonotoneX } from 'd3-shape' -import type { PremiumKpiId, PremiumKpiMetric, PremiumKpiPoint } from './model' - -const monotone = d3Curve(curveMonotoneX) -const accentFallbacks = { - revenue: '#6d5dfc', - customers: '#0f91c7', - churn: '#0c9b6c', -} satisfies Record - -export const premiumKpiSpring = { - type: 'spring' as const, - stiffness: 180, - damping: 24, - mass: 0.8, -} - -export function premiumKpiDefinition(metric: PremiumKpiMetric) { - const values = metric.rows.map((row) => row.value) - const minimum = Math.min(...values) - const maximum = Math.max(...values) - const padding = Math.max((maximum - minimum) * 0.16, 0.1) - const baseline = minimum - padding - const accent = `var(--premium-kpi-accent, ${accentFallbacks[metric.id]})` - const line = lineY(metric.rows, { - id: `${metric.id}-line`, - x: 'period', - y: 'value', - key: 'id', - stroke: accent, - strokeWidth: 2.4, - curve: monotone, - }) - const marks = - metric.surface === 'area' - ? [ - areaY(metric.rows, { - id: `${metric.id}-area`, - x: 'period', - y: 'value', - y1: baseline, - key: 'id', - fill: `url(#${metric.id}-fill)`, - fillOpacity: 1, - curve: monotone, - }), - line, - ] - : [line] - - return defineChart( - defineChart({ - guides: false, - marks, - gradients: - metric.surface === 'area' - ? [ - { - id: `${metric.id}-fill`, - x1: 0, - y1: 0, - x2: 0, - y2: 1, - stops: [ - { - offset: 0, - color: accent, - opacity: 0.26, - }, - { - offset: 1, - color: accent, - opacity: 0, - }, - ], - }, - ] - : [], - x: { - scale: scalePoint() - .domain(metric.rows.map((row) => row.period)) - .padding(0.08), - axis: false, - }, - y: { - scale: scaleLinear().domain([baseline, maximum + padding]), - axis: false, - }, - margin: { top: 4, right: 3, bottom: 3, left: 3 }, - clip: true, - motion: { transition: premiumKpiSpring }, - }), - { - focus: false, - pointer: false, - keyboard: false, - tooltip: false, - svgAnimation: false, - }, - ) -} - -export function createPremiumKpiRenderer(initial = true) { - return motion({ - initial, - respectReducedMotion: true, - transition: premiumKpiSpring, - }) -} diff --git a/benchmarks/conformance/cases/122-premium-kpi-sparklines/example.tsx b/benchmarks/conformance/cases/122-premium-kpi-sparklines/example.tsx new file mode 100644 index 00000000..485a5907 --- /dev/null +++ b/benchmarks/conformance/cases/122-premium-kpi-sparklines/example.tsx @@ -0,0 +1,386 @@ +import { useMemo, useRef } from 'react' +import { Chart } from '@tanstack/charts/react/core' +import { premiumKpisForRevision } from './model' +import type { CSSProperties } from 'react' +import type { PremiumKpiMetric } from './model' +import { areaY, d3Curve, defineChart, lineY } from '@tanstack/charts' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from '@tanstack/charts/scales/linear' +import { scalePoint } from '@tanstack/charts/scales/point' +import { curveMonotoneX } from 'd3-shape' +import type { PremiumKpiId, PremiumKpiPoint } from './model' +const fullGap = 12 + +function KpiCard({ + metric, + width, + height, + idPrefix, + preview = false, +}: { + readonly metric: PremiumKpiMetric + readonly width: number + readonly height: number + readonly idPrefix?: string + readonly preview?: boolean +}) { + const definition = useMemo(() => premiumKpiDefinition(metric), [metric]) + const renderer = useMemo(() => createPremiumKpiRenderer(!preview), [preview]) + const compact = preview || width < 200 || height < 150 + const padding = compact ? 9 : 18 + const headerHeight = compact ? 44 : 72 + const chartWidth = Math.max(24, width - padding * 2) + const chartHeight = Math.max(24, height - padding * 2 - headerHeight) + + return ( +
+
+ {metric.label} + + {' '} + {metric.trend} + + {metric.value} +
+
+ +
+
+ ) +} + +function fullLayout(width: number, height: number) { + const padding = 14 + const availableWidth = Math.max(1, width - padding * 2) + const availableHeight = Math.max(1, height - padding * 2) + const horizontal = width >= 680 || width / height >= 1.2 + + return horizontal + ? { + columns: 'repeat(3, minmax(0, 1fr))', + rows: 'minmax(0, 1fr)', + cardWidth: Math.max(1, (availableWidth - fullGap * 2) / 3), + cardHeight: availableHeight, + } + : { + columns: 'minmax(0, 1fr)', + rows: 'repeat(3, minmax(0, 1fr))', + cardWidth: availableWidth, + cardHeight: Math.max(1, (availableHeight - fullGap * 2) / 3), + } +} + +const premiumKpiStyles = ` +.premium-kpi-shell { + --premium-kpi-canvas: light-dark(#f5f6f8, #09090b); + --premium-kpi-card: light-dark(rgba(255, 255, 255, 0.92), rgba(24, 24, 27, 0.9)); + --premium-kpi-foreground: light-dark(#18181b, #fafafa); + --premium-kpi-muted: light-dark(#71717a, #a1a1aa); + --premium-kpi-border: light-dark(rgba(24, 24, 27, 0.09), rgba(250, 250, 250, 0.1)); + --premium-kpi-shadow: light-dark(rgba(24, 24, 27, 0.07), rgba(0, 0, 0, 0.34)); + color-scheme: inherit; + box-sizing: border-box; + display: grid; + place-items: stretch; + padding: 14px; + overflow: hidden; + color: var(--premium-kpi-foreground); + background: var(--premium-kpi-canvas); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.premium-kpi-grid { + display: grid; + min-width: 0; + min-height: 0; +} + +.premium-kpi-card { + --premium-kpi-accent: #6d5dfc; + box-sizing: border-box; + display: flex; + flex-direction: column; + width: var(--premium-kpi-card-width); + height: var(--premium-kpi-card-height); + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 18px; + border: 1px solid var(--premium-kpi-border); + border-radius: 18px; + background: var(--premium-kpi-card); + box-shadow: 0 14px 42px var(--premium-kpi-shadow); +} + +.premium-kpi-card[data-premium-kpi="customers"] { + --premium-kpi-accent: #0f91c7; +} + +.premium-kpi-card[data-premium-kpi="churn"] { + --premium-kpi-accent: #0c9b6c; +} + +.premium-kpi-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: baseline; + column-gap: 8px; + flex: 0 0 72px; +} + +.premium-kpi-label { + overflow: hidden; + color: var(--premium-kpi-muted); + font-size: 12px; + font-weight: 620; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.premium-kpi-value { + grid-column: 1 / -1; + margin-top: 7px; + font-size: clamp(24px, 3.1vw, 36px); + font-weight: 730; + letter-spacing: -0.055em; + line-height: 1; +} + +.premium-kpi-trend { + color: var(--premium-kpi-accent); + font-size: 11px; + font-weight: 700; + line-height: 1; + white-space: nowrap; +} + +.premium-kpi-chart { + display: grid; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + align-items: end; +} + +.premium-kpi-preview { + display: flex; + gap: 5px; + padding: 0; + background: transparent; +} + +.premium-kpi-card[data-compact] { + padding: 9px; + border-radius: 11px; + box-shadow: 0 6px 18px var(--premium-kpi-shadow); +} + +.premium-kpi-card[data-compact] .premium-kpi-header { + flex-basis: 44px; + column-gap: 4px; +} + +.premium-kpi-card[data-compact] .premium-kpi-label { + font-size: 7px; +} + +.premium-kpi-card[data-compact] .premium-kpi-value { + margin-top: 3px; + font-size: 15px; +} + +.premium-kpi-card[data-compact] .premium-kpi-trend { + font-size: 7px; +} + +.premium-kpi-preview-stack { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; +} + +` + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const monotone = d3Curve(curveMonotoneX) + +const accentFallbacks = { + revenue: '#6d5dfc', + customers: '#0f91c7', + churn: '#0c9b6c', +} satisfies Record + +export const premiumKpiSpring = { + type: 'spring' as const, + stiffness: 180, + damping: 24, + mass: 0.8, +} + +export function premiumKpiDefinition(metric: PremiumKpiMetric) { + const values = metric.rows.map((row) => row.value) + const minimum = Math.min(...values) + const maximum = Math.max(...values) + const padding = Math.max((maximum - minimum) * 0.16, 0.1) + const baseline = minimum - padding + const accent = `var(--premium-kpi-accent, ${accentFallbacks[metric.id]})` + const line = lineY(metric.rows, { + id: `${metric.id}-line`, + x: 'period', + y: 'value', + key: 'id', + stroke: accent, + strokeWidth: 2.4, + curve: monotone, + }) + const marks = + metric.surface === 'area' + ? [ + areaY(metric.rows, { + id: `${metric.id}-area`, + x: 'period', + y: 'value', + y1: baseline, + key: 'id', + fill: `url(#${metric.id}-fill)`, + fillOpacity: 1, + curve: monotone, + }), + line, + ] + : [line] + + return defineChart( + defineChart({ + guides: false, + marks, + gradients: + metric.surface === 'area' + ? [ + { + id: `${metric.id}-fill`, + x1: 0, + y1: 0, + x2: 0, + y2: 1, + stops: [ + { + offset: 0, + color: accent, + opacity: 0.26, + }, + { + offset: 1, + color: accent, + opacity: 0, + }, + ], + }, + ] + : [], + x: { + scale: scalePoint() + .domain(metric.rows.map((row) => row.period)) + .padding(0.08), + axis: false, + }, + y: { + scale: scaleLinear().domain([baseline, maximum + padding]), + axis: false, + }, + margin: { top: 4, right: 3, bottom: 3, left: 3 }, + clip: true, + motion: { transition: premiumKpiSpring }, + }), + { + focus: false, + pointer: false, + keyboard: false, + tooltip: false, + svgAnimation: false, + }, + ) +} + +export function createPremiumKpiRenderer(initial = true) { + return motion({ + initial, + respectReducedMotion: true, + transition: premiumKpiSpring, + }) +} + +export default function PremiumKpiSparklines({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '122-premium-kpi-sparklines' + const rootRef = useRef(null) + + const metrics = premiumKpisForRevision(input.revision) + + const layout = fullLayout(input.width, input.height) + + return ( +
+ +
+ {metrics.map((metric) => ( + + ))} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/122-premium-kpi-sparklines/tanstack.ts b/benchmarks/conformance/cases/122-premium-kpi-sparklines/tanstack.ts index 7689eb98..fd7a50f2 100644 --- a/benchmarks/conformance/cases/122-premium-kpi-sparklines/tanstack.ts +++ b/benchmarks/conformance/cases/122-premium-kpi-sparklines/tanstack.ts @@ -2,5 +2,6 @@ export { createPremiumKpiRenderer, premiumKpiDefinition, premiumKpiSpring, -} from './chart' +} from './example' +export { default as Example } from './example' export { catalogComponent, mount } from './view' diff --git a/benchmarks/conformance/cases/122-premium-kpi-sparklines/view.tsx b/benchmarks/conformance/cases/122-premium-kpi-sparklines/view.tsx index ef535378..b8fff84b 100644 --- a/benchmarks/conformance/cases/122-premium-kpi-sparklines/view.tsx +++ b/benchmarks/conformance/cases/122-premium-kpi-sparklines/view.tsx @@ -1,7 +1,7 @@ import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' import { Chart } from '@tanstack/charts/react/core' import { settleChartMotion } from '../../shared/motion' -import { createPremiumKpiRenderer, premiumKpiDefinition } from './chart' +import { createPremiumKpiRenderer, premiumKpiDefinition } from './example' import { premiumKpisForRevision } from './model' import { reactMount } from '../../shared/react-mount' import type { CSSProperties, RefObject } from 'react' diff --git a/benchmarks/conformance/cases/123-active-donut-metric/chart.ts b/benchmarks/conformance/cases/123-active-donut-metric/chart.ts deleted file mode 100644 index b465582f..00000000 --- a/benchmarks/conformance/cases/123-active-donut-metric/chart.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { defineChart } from '@tanstack/charts' -import { decorative } from '@tanstack/charts/mark/decorative' -import { pie, polar, radialArc, radialText } from '@tanstack/charts/polar' -import { scaleLinear } from '@tanstack/charts/scales/linear' -import { tooltip } from '@tanstack/charts/tooltip' -import { browserRows, browserTotal } from './model' -import type { ChartTooltipOptions } from '@tanstack/charts' -import type { PieDatum } from '@tanstack/charts/polar' -import type { ConformanceInput } from '../../types' -import type { BrowserRow } from './model' - -const tau = Math.PI * 2 -const gapAngle = (Math.PI / 180) * 2.8 -export const donutSpring = { - type: 'spring' as const, - stiffness: 190, - damping: 21, - mass: 0.82, -} - -export interface DonutCenterRow { - id: string - angle: number - radius: number - text: string - dy: number -} - -export type DonutDatum = PieDatum | DonutCenterRow - -const tooltipOptions: ChartTooltipOptions = { - anchor: 'point', - placement: ['top', 'right', 'left', 'bottom'], - className: 'active-donut-tooltip', - format: (point) => - 'visitors' in point.datum - ? `${point.datum.label} · ${point.datum.visitors.toLocaleString('en-US')} visitors` - : point.datum.text, -} - -export function activeDonutDefinition( - input: ConformanceInput, - activeId: string, -) { - const rows = browserRows(input.revision) - const selected = rows.find((row) => row.id === activeId) ?? rows[0]! - const { arcs, active } = activeDonutArcs(rows, selected.id) - const centerRows: readonly DonutCenterRow[] = [ - { - id: `${selected.id}:value`, - angle: 0, - radius: 0, - text: selected.visitors.toLocaleString('en-US'), - dy: -8, - }, - { - id: `${selected.id}:label`, - angle: 0, - radius: 0, - text: selected.label, - dy: input.preview ? 12 : 15, - }, - ] - - return defineChart( - { - marks: [ - polar({ - id: 'browser-donut', - radiusRatio: input.preview ? 0.76 : 0.74, - marks: [ - radialArc(arcs, { - id: 'browser-arcs', - key: 'id', - color: 'id', - innerRadius: ({ radius }) => radius * 0.6, - cornerRadius: 7, - motion: { transition: donutSpring }, - }), - ], - }), - decorative( - polar({ - id: 'selected-browser', - radiusRatio: input.preview ? 0.84 : 0.83, - marks: [ - radialArc(active, { - id: 'selected-browser-wedge', - key: 'id', - color: 'id', - innerRadius: ({ radius }) => radius * 0.59, - cornerRadius: 8, - stroke: 'Canvas', - strokeWidth: input.preview ? 1.5 : 2.5, - motion: { transition: donutSpring }, - }), - radialArc(active, { - id: 'selected-browser-ring', - key: 'id', - color: 'id', - innerRadius: ({ radius }) => radius * 0.94, - outerRadius: ({ radius }) => radius, - cornerRadius: 4, - fillOpacity: 0.78, - motion: { - delay: 70, - transition: donutSpring, - }, - }), - ], - }), - ), - decorative( - polar({ - id: 'donut-center', - radiusRatio: 0.8, - angle: { scale: scaleLinear().domain([0, tau]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialText(centerRows.slice(0, 1), { - id: 'donut-center-value', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: (row) => row.dy, - fill: 'currentColor', - fontSize: input.preview ? 19 : 26, - fontWeight: 760, - motion: { transition: donutSpring }, - }), - radialText(centerRows.slice(1), { - id: 'donut-center-label', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: (row) => row.dy, - fill: 'color-mix(in srgb, currentColor 56%, transparent)', - fontSize: input.preview ? 8 : 11, - fontWeight: 620, - motion: { transition: donutSpring }, - }), - ], - }), - ), - ], - color: { - domain: rows.map((row) => row.id), - range: [ - 'var(--ts-chart-1)', - 'var(--ts-chart-2)', - 'var(--ts-chart-3)', - 'var(--ts-chart-4)', - 'var(--ts-chart-5)', - ], - }, - guides: false, - margin: 0, - motion: { transition: donutSpring }, - }, - { - focus: 'nearest', - focusRing: false, - keyboard: input.interactive, - tooltip: input.interactive ? { use: tooltip, ...tooltipOptions } : false, - }, - ) -} - -export function activeDonutArcs(rows: readonly BrowserRow[], activeId: string) { - const arcs = pie(rows, { value: 'visitors', gapAngle }) - return { - arcs, - active: arcs.filter((row) => row.id === activeId), - } -} - -export function donutSummary(input: ConformanceInput, activeId: string) { - const rows = browserRows(input.revision) - const selected = rows.find((row) => row.id === activeId) ?? rows[0]! - return { - selected, - total: browserTotal(rows), - share: selected.visitors / browserTotal(rows), - } -} diff --git a/benchmarks/conformance/cases/123-active-donut-metric/example.tsx b/benchmarks/conformance/cases/123-active-donut-metric/example.tsx new file mode 100644 index 00000000..f889cd4b --- /dev/null +++ b/benchmarks/conformance/cases/123-active-donut-metric/example.tsx @@ -0,0 +1,385 @@ +import { useMemo, useRef, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { activeDonutLayout } from './layout' +import { browserRows } from './model' +import type { CSSProperties } from 'react' +import type { ChartPoint } from '@tanstack/charts' +import { defineChart } from '@tanstack/charts' +import { decorative } from '@tanstack/charts/mark/decorative' +import { pie, polar, radialArc, radialText } from '@tanstack/charts/polar' +import { scaleLinear } from '@tanstack/charts/scales/linear' +import { tooltip } from '@tanstack/charts/tooltip' +import { browserTotal } from './model' +import type { ChartTooltipOptions } from '@tanstack/charts' +import type { PieDatum } from '@tanstack/charts/polar' +import type { BrowserRow } from './model' +const palette = ['#7c3aed', '#06b6d4', '#f97316', '#ec4899', '#84cc16'] + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const tau = Math.PI * 2 + +const gapAngle = (Math.PI / 180) * 2.8 + +export const donutSpring = { + type: 'spring' as const, + stiffness: 190, + damping: 21, + mass: 0.82, +} + +export interface DonutCenterRow { + id: string + angle: number + radius: number + text: string + dy: number +} + +export type DonutDatum = PieDatum | DonutCenterRow + +const tooltipOptions: ChartTooltipOptions = { + anchor: 'point', + placement: ['top', 'right', 'left', 'bottom'], + className: 'active-donut-tooltip', + format: (point) => + 'visitors' in point.datum + ? `${point.datum.label} · ${point.datum.visitors.toLocaleString('en-US')} visitors` + : point.datum.text, +} + +export function activeDonutDefinition( + input: ExampleChartInput, + activeId: string, +) { + const rows = browserRows(input.revision) + const selected = rows.find((row) => row.id === activeId) ?? rows[0]! + const { arcs, active } = activeDonutArcs(rows, selected.id) + const centerRows: readonly DonutCenterRow[] = [ + { + id: `${selected.id}:value`, + angle: 0, + radius: 0, + text: selected.visitors.toLocaleString('en-US'), + dy: -8, + }, + { + id: `${selected.id}:label`, + angle: 0, + radius: 0, + text: selected.label, + dy: input.preview ? 12 : 15, + }, + ] + + return defineChart( + { + marks: [ + polar({ + id: 'browser-donut', + radiusRatio: input.preview ? 0.76 : 0.74, + marks: [ + radialArc(arcs, { + id: 'browser-arcs', + key: 'id', + color: 'id', + innerRadius: ({ radius }) => radius * 0.6, + cornerRadius: 7, + motion: { transition: donutSpring }, + }), + ], + }), + decorative( + polar({ + id: 'selected-browser', + radiusRatio: input.preview ? 0.84 : 0.83, + marks: [ + radialArc(active, { + id: 'selected-browser-wedge', + key: 'id', + color: 'id', + innerRadius: ({ radius }) => radius * 0.59, + cornerRadius: 8, + stroke: 'Canvas', + strokeWidth: input.preview ? 1.5 : 2.5, + motion: { transition: donutSpring }, + }), + radialArc(active, { + id: 'selected-browser-ring', + key: 'id', + color: 'id', + innerRadius: ({ radius }) => radius * 0.94, + outerRadius: ({ radius }) => radius, + cornerRadius: 4, + fillOpacity: 0.78, + motion: { + delay: 70, + transition: donutSpring, + }, + }), + ], + }), + ), + decorative( + polar({ + id: 'donut-center', + radiusRatio: 0.8, + angle: { scale: scaleLinear().domain([0, tau]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialText(centerRows.slice(0, 1), { + id: 'donut-center-value', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: (row) => row.dy, + fill: 'currentColor', + fontSize: input.preview ? 19 : 26, + fontWeight: 760, + motion: { transition: donutSpring }, + }), + radialText(centerRows.slice(1), { + id: 'donut-center-label', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: (row) => row.dy, + fill: 'color-mix(in srgb, currentColor 56%, transparent)', + fontSize: input.preview ? 8 : 11, + fontWeight: 620, + motion: { transition: donutSpring }, + }), + ], + }), + ), + ], + color: { + domain: rows.map((row) => row.id), + range: [ + 'var(--ts-chart-1)', + 'var(--ts-chart-2)', + 'var(--ts-chart-3)', + 'var(--ts-chart-4)', + 'var(--ts-chart-5)', + ], + }, + guides: false, + margin: 0, + motion: { transition: donutSpring }, + }, + { + focus: 'nearest', + focusRing: false, + keyboard: input.interactive, + tooltip: input.interactive ? { use: tooltip, ...tooltipOptions } : false, + }, + ) +} + +export function activeDonutArcs(rows: readonly BrowserRow[], activeId: string) { + const arcs = pie(rows, { value: 'visitors', gapAngle }) + return { + arcs, + active: arcs.filter((row) => row.id === activeId), + } +} + +export function donutSummary(input: ExampleChartInput, activeId: string) { + const rows = browserRows(input.revision) + const selected = rows.find((row) => row.id === activeId) ?? rows[0]! + return { + selected, + total: browserTotal(rows), + share: selected.visitors / browserTotal(rows), + } +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export default function ActiveDonutMetric({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '123-active-donut-metric' + const [activeId, setActiveId] = useState('chrome') + + const root = useRef(null) + + const chartHost = useRef(null) + + const buttonRefs = useRef(new Map()) + + const focusedId = useRef(null) + + const renderer = useMemo( + () => motion({ initial: !false }), + [false], + ) + + const definition = useMemo( + () => activeDonutDefinition(input, activeId), + [activeId, input], + ) + + const rows = browserRows(input.revision) + + const summary = donutSummary(input, activeId) + + const selectPoint = ( + point: ChartPoint | null, + ) => { + const row = point && 'visitors' in point.datum ? point.datum : null + focusedId.current = row?.id ?? null + if (row) setActiveId(row.id) + } + + const layout = activeDonutLayout(input.width, input.height, rows.length) + + const { chartSize, compact } = layout + + const cardStyle: CSSProperties & Record<`--${string}`, string> = { + '--ts-chart-1': palette[0]!, + '--ts-chart-2': palette[1]!, + '--ts-chart-3': palette[2]!, + '--ts-chart-4': palette[3]!, + '--ts-chart-5': palette[4]!, + '--ts-chart-tooltip-background': + 'color-mix(in srgb, Canvas 94%, transparent)', + '--ts-chart-tooltip-color': 'CanvasText', + '--ts-chart-tooltip-border': + '1px solid color-mix(in srgb, CanvasText 12%, transparent)', + '--ts-chart-tooltip-border-radius': '10px', + '--ts-chart-tooltip-shadow': + '0 12px 34px color-mix(in srgb, CanvasText 14%, transparent)', + '--ts-chart-tooltip-font': + '600 12px/1.35 Inter, ui-sans-serif, system-ui, sans-serif', + boxSizing: 'border-box', + width: input.width, + height: input.height, + overflow: 'hidden', + border: '1px solid color-mix(in srgb, currentColor 12%, transparent)', + borderRadius: 18, + color: 'CanvasText', + background: 'Canvas', + fontFamily: + "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + boxShadow: '0 18px 55px color-mix(in srgb, CanvasText 7%, transparent)', + } + + return ( +
+
+
Browser visitors
+
+ {summary.total.toLocaleString('en-US')} sessions +
+
+
+ + idPrefix={idPrefix} + definition={definition} + renderer={renderer} + width={chartSize} + height={chartSize} + ariaLabel="Browser visitor share" + onRender={({ container }) => { + chartHost.current = container + }} + onFocusChange={selectPoint} + /> +
+ {rows.map((row, index) => { + const selected = row.id === activeId + return ( + + ) + })} +
+
+
+ ) +} diff --git a/benchmarks/conformance/cases/123-active-donut-metric/tanstack.test.ts b/benchmarks/conformance/cases/123-active-donut-metric/tanstack.test.ts index 33989301..1a4a3a87 100644 --- a/benchmarks/conformance/cases/123-active-donut-metric/tanstack.test.ts +++ b/benchmarks/conformance/cases/123-active-donut-metric/tanstack.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { createChartScene, type SceneNode } from '@tanstack/charts' -import { activeDonutArcs, activeDonutDefinition, donutSummary } from './chart' +import { activeDonutArcs, activeDonutDefinition, donutSummary } from './example' import { activeDonutLayout } from './layout' import { mount as rechartsMount, sectorAngles } from './recharts' import { mount as tanstackMount } from './view' diff --git a/benchmarks/conformance/cases/123-active-donut-metric/tanstack.ts b/benchmarks/conformance/cases/123-active-donut-metric/tanstack.ts index 3c16c4f4..fe87d234 100644 --- a/benchmarks/conformance/cases/123-active-donut-metric/tanstack.ts +++ b/benchmarks/conformance/cases/123-active-donut-metric/tanstack.ts @@ -1,2 +1,3 @@ -export { activeDonutDefinition } from './chart' +export { default as Example } from './example' +export { activeDonutDefinition } from './example' export { mount } from './view' diff --git a/benchmarks/conformance/cases/123-active-donut-metric/view.tsx b/benchmarks/conformance/cases/123-active-donut-metric/view.tsx index 6b068072..adcce9b6 100644 --- a/benchmarks/conformance/cases/123-active-donut-metric/view.tsx +++ b/benchmarks/conformance/cases/123-active-donut-metric/view.tsx @@ -8,7 +8,7 @@ import { import { motion } from '@tanstack/charts/motion' import { Chart } from '@tanstack/charts/react/core' import { settleChartMotion } from '../../shared/motion' -import { activeDonutDefinition, donutSummary } from './chart' +import { activeDonutDefinition, donutSummary } from './example' import { activeDonutLayout } from './layout' import { browserRows } from './model' import { reactMount } from '../../shared/react-mount' @@ -16,7 +16,7 @@ import type { CSSProperties } from 'react' import type { ChartPoint } from '@tanstack/charts' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' -import type { DonutDatum } from './chart' +import type { DonutDatum } from './example' const palette = ['#7c3aed', '#06b6d4', '#f97316', '#ec4899', '#84cc16'] diff --git a/benchmarks/conformance/cases/124-theme-palette-matrix/chart.ts b/benchmarks/conformance/cases/124-theme-palette-matrix/chart.ts deleted file mode 100644 index cca3bde5..00000000 --- a/benchmarks/conformance/cases/124-theme-palette-matrix/chart.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { areaY, defineChart, dot, lineY } from '@tanstack/charts' -import { scaleLinear } from '@tanstack/charts/scales/linear' -import { scalePoint } from '@tanstack/charts/scales/point' -import { palettePaint } from './model' -import type { PaletteMatrixRow, PaletteTreatment } from './model' - -const gradientId = 'palette-area' - -export function paletteMatrixDefinition( - rows: readonly PaletteMatrixRow[], - treatment: PaletteTreatment, - preview = false, -) { - const primary = palettePaint(treatment, 'primary') - const secondary = palettePaint(treatment, 'secondary') - const surface = palettePaint(treatment, 'surface') - - return defineChart({ - motion: { - transition: { - type: 'spring', - stiffness: 180, - damping: 22, - mass: 0.8, - }, - }, - marks: [ - areaY(rows, { - id: 'value-area', - x: 'period', - y: 'value', - key: 'id', - fill: `url(#${gradientId})`, - }), - lineY(rows, { - id: 'value-line', - x: 'period', - y: 'value', - key: 'id', - stroke: primary, - strokeWidth: preview ? 2 : 2.5, - }), - lineY(rows, { - id: 'comparison-line', - x: 'period', - y: 'comparison', - key: 'id', - stroke: secondary, - strokeOpacity: 0.8, - strokeWidth: preview ? 1 : 1.5, - strokeDasharray: '3 4', - motion: { - transition: { type: 'tween', duration: 420, easing: 'ease-out' }, - }, - }), - dot(rows.slice(-1), { - id: 'latest-value', - x: 'period', - y: 'value', - key: 'id', - r: preview ? 2 : 3.5, - fill: primary, - stroke: surface, - strokeWidth: preview ? 1 : 2, - motion: { transition: { type: 'spring', mass: 1.15 } }, - }), - ], - x: { - scale: () => - scalePoint() - .domain(rows.map((row) => row.period)) - .padding(0.12), - }, - y: { scale: scaleLinear().domain([20, 100]) }, - guides: false, - margin: preview ? 5 : { top: 12, right: 14, bottom: 10, left: 14 }, - gradients: [ - { - id: gradientId, - x1: 0, - y1: 1, - x2: 0, - y2: 0, - stops: [ - { offset: 0, color: primary, opacity: 0.02 }, - { offset: 0.55, color: primary, opacity: 0.16 }, - { offset: 1, color: primary, opacity: 0.52 }, - ], - }, - ], - clip: true, - theme: { - background: surface, - foreground: palettePaint(treatment, 'foreground'), - muted: palettePaint(treatment, 'muted'), - grid: palettePaint(treatment, 'grid'), - palette: [primary, secondary], - }, - focus: false, - keyboard: false, - tooltip: false, - }) -} diff --git a/benchmarks/conformance/cases/124-theme-palette-matrix/example.tsx b/benchmarks/conformance/cases/124-theme-palette-matrix/example.tsx new file mode 100644 index 00000000..e2b53757 --- /dev/null +++ b/benchmarks/conformance/cases/124-theme-palette-matrix/example.tsx @@ -0,0 +1,270 @@ +import { useMemo, useRef } from 'react' +import { motion } from '@tanstack/charts/motion' +import { Chart } from '@tanstack/charts/react/core' +import { + paletteMatrixRows, + palettePaint, + paletteTreatments, + paletteValue, + paletteVariable, +} from './model' +import type { CSSProperties } from 'react' +import type { PaletteMatrixRow, PaletteTreatment } from './model' +import { areaY, defineChart, dot, lineY } from '@tanstack/charts' +import { scaleLinear } from '@tanstack/charts/scales/linear' +import { scalePoint } from '@tanstack/charts/scales/point' +type PalettePanelStyle = CSSProperties & Record<`--ts-matrix-${string}`, string> + +function palettePanelStyle(treatment: PaletteTreatment): PalettePanelStyle { + return Object.fromEntries( + Object.keys(treatment.tokens).map((token) => [ + paletteVariable(treatment, token as keyof PaletteTreatment['tokens']), + paletteValue(treatment, token as keyof PaletteTreatment['tokens']), + ]), + ) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const gradientId = 'palette-area' + +export function paletteMatrixDefinition( + rows: readonly PaletteMatrixRow[], + treatment: PaletteTreatment, + preview = false, +) { + const primary = palettePaint(treatment, 'primary') + const secondary = palettePaint(treatment, 'secondary') + const surface = palettePaint(treatment, 'surface') + + return defineChart({ + motion: { + transition: { + type: 'spring', + stiffness: 180, + damping: 22, + mass: 0.8, + }, + }, + marks: [ + areaY(rows, { + id: 'value-area', + x: 'period', + y: 'value', + key: 'id', + fill: `url(#${gradientId})`, + }), + lineY(rows, { + id: 'value-line', + x: 'period', + y: 'value', + key: 'id', + stroke: primary, + strokeWidth: preview ? 2 : 2.5, + }), + lineY(rows, { + id: 'comparison-line', + x: 'period', + y: 'comparison', + key: 'id', + stroke: secondary, + strokeOpacity: 0.8, + strokeWidth: preview ? 1 : 1.5, + strokeDasharray: '3 4', + motion: { + transition: { type: 'tween', duration: 420, easing: 'ease-out' }, + }, + }), + dot(rows.slice(-1), { + id: 'latest-value', + x: 'period', + y: 'value', + key: 'id', + r: preview ? 2 : 3.5, + fill: primary, + stroke: surface, + strokeWidth: preview ? 1 : 2, + motion: { transition: { type: 'spring', mass: 1.15 } }, + }), + ], + x: { + scale: () => + scalePoint() + .domain(rows.map((row) => row.period)) + .padding(0.12), + }, + y: { scale: scaleLinear().domain([20, 100]) }, + guides: false, + margin: preview ? 5 : { top: 12, right: 14, bottom: 10, left: 14 }, + gradients: [ + { + id: gradientId, + x1: 0, + y1: 1, + x2: 0, + y2: 0, + stops: [ + { offset: 0, color: primary, opacity: 0.02 }, + { offset: 0.55, color: primary, opacity: 0.16 }, + { offset: 1, color: primary, opacity: 0.52 }, + ], + }, + ], + clip: true, + theme: { + background: surface, + foreground: palettePaint(treatment, 'foreground'), + muted: palettePaint(treatment, 'muted'), + grid: palettePaint(treatment, 'grid'), + palette: [primary, secondary], + }, + focus: false, + keyboard: false, + tooltip: false, + }) +} + +export default function ThemePaletteMatrix({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '124-theme-palette-matrix' + const rootRef = useRef(null) + + const rows = useMemo( + () => paletteMatrixRows(input.revision), + [input.revision], + ) + + const definitions = useMemo( + () => + paletteTreatments.map((treatment) => + paletteMatrixDefinition(rows, treatment, false), + ), + [false, rows], + ) + + const renderer = useMemo( + () => + motion({ + initial: true, + respectReducedMotion: true, + transition: { + type: 'spring', + stiffness: 180, + damping: 22, + mass: 0.8, + }, + }), + [], + ) + + const gap = false ? 4 : 10 + + const padding = false ? 0 : 12 + + const availableHeight = input.height - padding * 2 - gap * 2 + + const panelHeight = Math.max(1, availableHeight / 3) + + const labelWidth = input.width < 440 ? 120 : 124 + + const chartWidth = false + ? input.width + : Math.max(1, input.width - padding * 2 - labelWidth) + + return ( +
+ {paletteTreatments.map((treatment, index) => { + const definition = definitions[index] + if (!definition) return null + const style = palettePanelStyle(treatment) + + return ( +
+ {false ? null : ( +
+
+ )} + +
+ ) + })} +
+ ) +} diff --git a/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.test.ts b/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.test.ts index 25cb43ad..42767954 100644 --- a/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.test.ts +++ b/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.test.ts @@ -1,7 +1,7 @@ import { act } from 'react' import { createChartScene } from '@tanstack/charts' import { describe, expect, it } from 'vitest' -import { paletteMatrixDefinition } from './chart' +import { paletteMatrixDefinition } from './example' import { paletteMatrixRows, palettePaint, paletteTreatments } from './model' import { mount as plotMount } from './plot' import { mount } from './tanstack' diff --git a/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.ts b/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.ts index 8edbf730..79ea2aea 100644 --- a/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.ts +++ b/benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.ts @@ -1,2 +1,3 @@ -export { paletteMatrixDefinition } from './chart' +export { default as Example } from './example' +export { paletteMatrixDefinition } from './example' export { catalogComponent, mount } from './view' diff --git a/benchmarks/conformance/cases/124-theme-palette-matrix/view.tsx b/benchmarks/conformance/cases/124-theme-palette-matrix/view.tsx index c9e2d6ed..f79e10eb 100644 --- a/benchmarks/conformance/cases/124-theme-palette-matrix/view.tsx +++ b/benchmarks/conformance/cases/124-theme-palette-matrix/view.tsx @@ -3,7 +3,7 @@ import { motion } from '@tanstack/charts/motion' import { Chart } from '@tanstack/charts/react/core' import { settleChartMotion } from '../../shared/motion' import { reactMount } from '../../shared/react-mount' -import { paletteMatrixDefinition } from './chart' +import { paletteMatrixDefinition } from './example' import { paletteMatrixRows, palettePaint, diff --git a/benchmarks/conformance/cases/125-sales-funnel/example.tsx b/benchmarks/conformance/cases/125-sales-funnel/example.tsx new file mode 100644 index 00000000..da0d5690 --- /dev/null +++ b/benchmarks/conformance/cases/125-sales-funnel/example.tsx @@ -0,0 +1,77 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaX, defineChart, text } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { funnelStagesForRevision } from './data' +import { funnelLayout } from './model' + +const colors = ['#1e3a8a', '#1d4ed8', '#2563eb', '#3b82f6', '#60a5fa'] +const firstStageValue = funnelStagesForRevision(0)[0]?.value ?? 1 + +export const salesFunnelDefinition = (input: ExampleOptions) => { + const stages = funnelStagesForRevision(input.revision) + const layout = funnelLayout(stages) + + return defineChart({ + marks: [ + areaX(layout.points, { + id: 'funnel-stages', + x1: 'x1', + x2: 'x2', + y: 'y', + z: 'id', + color: 'id', + key: (point) => `${point.id}:${point.boundary}`, + fillOpacity: 1, + }), + text(layout.labels, { + id: 'funnel-labels', + x: 'x', + y: 'y', + text: 'text', + key: 'id', + anchor: 'start', + fontSize: input.preview === true ? 8 : input.width < 400 ? 10 : 12, + fontWeight: 600, + }), + ], + x: { scale: scaleLinear().domain(layout.xDomain), axis: false }, + y: { scale: scaleLinear().domain(layout.yDomain), axis: false }, + color: { domain: stages.map((stage) => stage.id), range: colors }, + margin: 12, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Sales conversion funnel' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(salesFunnelDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.label} · ${datum.value.toLocaleString('en-US')} · ${Math.round( + (datum.value / firstStageValue) * 100, + )}% of visitors`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/125-sales-funnel/tanstack.ts b/benchmarks/conformance/cases/125-sales-funnel/tanstack.ts index 14a856eb..58c541f6 100644 --- a/benchmarks/conformance/cases/125-sales-funnel/tanstack.ts +++ b/benchmarks/conformance/cases/125-sales-funnel/tanstack.ts @@ -1,57 +1,12 @@ -import { areaX, defineChart, text } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { funnelStagesForRevision } from './data' -import { funnelLayout } from './model' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#1e3a8a', '#1d4ed8', '#2563eb', '#3b82f6', '#60a5fa'] -const firstStageValue = funnelStagesForRevision(0)[0]?.value ?? 1 +export * from './example' -export const salesFunnelDefinition = (input: ConformanceInput) => { - const stages = funnelStagesForRevision(input.revision) - const layout = funnelLayout(stages) - - return defineChart({ - marks: [ - areaX(layout.points, { - id: 'funnel-stages', - x1: 'x1', - x2: 'x2', - y: 'y', - z: 'id', - color: 'id', - key: (point) => `${point.id}:${point.boundary}`, - fillOpacity: 1, - }), - text(layout.labels, { - id: 'funnel-labels', - x: 'x', - y: 'y', - text: 'text', - key: 'id', - anchor: 'start', - fontSize: input.preview === true ? 8 : input.width < 400 ? 10 : 12, - fontWeight: 600, - }), - ], - x: { scale: scaleLinear().domain(layout.xDomain), axis: false }, - y: { scale: scaleLinear().domain(layout.yDomain), axis: false }, - color: { domain: stages.map((stage) => stage.id), range: colors }, - margin: 12, - }) -} - -export const catalogCase = tanstackCase( - salesFunnelDefinition, - 'Sales conversion funnel', - { - format: ({ datum }) => - `${datum.label} · ${datum.value.toLocaleString('en-US')} · ${Math.round( - (datum.value / firstStageValue) * 100, - )}% of visitors`, - }, +export const mount = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { margin: true }, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/126-drillable-sunburst/example.tsx b/benchmarks/conformance/cases/126-drillable-sunburst/example.tsx new file mode 100644 index 00000000..abf6860d --- /dev/null +++ b/benchmarks/conformance/cases/126-drillable-sunburst/example.tsx @@ -0,0 +1,153 @@ +import { useMemo, useState } from 'react' +import { motion } from '@tanstack/charts/motion' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { + flareAggregateValue, + flareHasChildren, + flareLabel, + flareParentId, + flarePreviewRootId, + formatFlareValue, +} from './model' +import type { FlareRow } from '@charts-poc/demo-data/flare' +import type { ChartPoint } from '@tanstack/charts' +import type { SunburstNode } from '@tanstack/charts/hierarchy/sunburst' +import { defineChart } from '@tanstack/charts' +import { sunburst } from '@tanstack/charts/hierarchy/sunburst' +import { polar } from '@tanstack/charts/polar' +import { tooltip } from '@tanstack/charts/tooltip' +import { + flareNodeColor, + flareRows, + flareVisibleDepth, + flareVisibleRingCount, +} from './model' +type DrillDatum = SunburstNode + +const tau = Math.PI * 2 + +const ringPadding = 2 + +export function drillableSunburstDefinition(rootId: string) { + const ringCount = flareVisibleRingCount(rootId) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.92, + startAngle: Math.PI / 2, + endAngle: Math.PI / 2 - tau, + marks: [ + sunburst(flareRows(), { + id: 'drillable-sunburst-arcs', + path: 'name', + delimiter: '.', + value: 'size', + rootId, + visibleDepth: flareVisibleDepth(rootId), + sort: (left, right) => + right.value - left.value || left.name.localeCompare(right.name), + innerRadius: ({ radius }) => radius * 0.32, + outerRadius: ({ radius }) => { + const innerRadius = radius * 0.32 + return ( + innerRadius + + ((radius - innerRadius) * ringCount) / (ringCount + 1) + + Math.max(0, ringCount - 1) * ringPadding + ) + }, + ringPadding, + fill: (node) => flareNodeColor(node.id), + stroke: 'Canvas', + strokeOpacity: 0.9, + strokeWidth: 2, + motion(context) { + return { + delay: + context.phase === 'enter' + ? Math.min(context.datumIndex * 18, 160) + : 0, + transition: + context.phase === 'exit' + ? { type: 'tween', duration: 320, easing: 'ease-out' } + : undefined, + } + }, + }), + ], + }), + ], + motion: { + transition: { type: 'tween', duration: 720, easing: 'ease-in-out' }, + }, + tooltip: { + use: tooltip, + format: ({ datum }) => `${datum.name} · ${formatFlareValue(datum.value)}`, + }, + keyboard: true, + margin: 0, + }) +} + +export default function Example() { + const [rootId, setRootId] = useState(flarePreviewRootId) + const definition = useMemo( + () => drillableSunburstDefinition(rootId), + [rootId], + ) + const renderer = useMemo( + () => motion({ initial: 'always' }), + [], + ) + const parentId = flareParentId(rootId) + + const drill = (point: ChartPoint | null) => { + if (point && flareHasChildren(point.datum.id)) setRootId(point.datum.id) + } + + return ( +
+ + +
+ ) +} diff --git a/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.test.ts b/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.test.ts index 3b9ef597..08079d7e 100644 --- a/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.test.ts +++ b/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.test.ts @@ -244,7 +244,7 @@ describe('drillable native sunburst', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts', + 'benchmarks/conformance/cases/126-drillable-sunburst/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts b/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts index 3107258d..1046e6ea 100644 --- a/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts +++ b/benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts @@ -1,9 +1,7 @@ -import { defineChart } from '@tanstack/charts' -import { sunburst } from '@tanstack/charts/hierarchy/sunburst' +import { drillableSunburstDefinition } from './example' +export { drillableSunburstDefinition } from './example' import { motion } from '@tanstack/charts/motion' -import { polar } from '@tanstack/charts/polar' import { mountChartRenderer } from '@tanstack/charts/renderer' -import { tooltip } from '@tanstack/charts/tooltip' import { readChartMotionState, settleChartMotion } from '../../shared/motion' import { createSunburstCenterControl, @@ -15,88 +13,17 @@ import { flareAggregateValue, flareHasChildren, flareLabel, - flareNodeColor, flareParentId, flarePreviewRootId, - flareRows, - flareVisibleDepth, - flareVisibleRingCount, - formatFlareValue, } from './model' import type { FlareRow } from '@charts-poc/demo-data/flare' import type { SunburstNode } from '@tanstack/charts/hierarchy/sunburst' import type { ChartPoint, ChartRendererHostOptions } from '@tanstack/charts' -import type { - ConformanceInput, - ConformanceMount, - ConformanceTestDriver, -} from '../../types' +import type { ConformanceMount, ConformanceTestDriver } from '../../types' -type DrillDatum = SunburstNode - -const tau = Math.PI * 2 -const ringPadding = 2 +export { default as Example } from './example' -export function drillableSunburstDefinition(rootId: string) { - const ringCount = flareVisibleRingCount(rootId) - - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.92, - startAngle: Math.PI / 2, - endAngle: Math.PI / 2 - tau, - marks: [ - sunburst(flareRows(), { - id: 'drillable-sunburst-arcs', - path: 'name', - delimiter: '.', - value: 'size', - rootId, - visibleDepth: flareVisibleDepth(rootId), - sort: (left, right) => - right.value - left.value || left.name.localeCompare(right.name), - innerRadius: ({ radius }) => radius * 0.32, - outerRadius: ({ radius }) => { - const innerRadius = radius * 0.32 - return ( - innerRadius + - ((radius - innerRadius) * ringCount) / (ringCount + 1) + - Math.max(0, ringCount - 1) * ringPadding - ) - }, - ringPadding, - fill: (node) => flareNodeColor(node.id), - stroke: 'Canvas', - strokeOpacity: 0.9, - strokeWidth: 2, - motion(context) { - return { - delay: - context.phase === 'enter' - ? Math.min(context.datumIndex * 18, 160) - : 0, - transition: - context.phase === 'exit' - ? { type: 'tween', duration: 320, easing: 'ease-out' } - : undefined, - } - }, - }), - ], - }), - ], - motion: { - transition: { type: 'tween', duration: 720, easing: 'ease-in-out' }, - }, - tooltip: { - use: tooltip, - format: ({ datum }) => `${datum.name} · ${formatFlareValue(datum.value)}`, - }, - keyboard: true, - margin: 0, - }) -} +type DrillDatum = SunburstNode export const mount: ConformanceMount = (container, input) => { let currentInput = input diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx b/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx index f1bfa373..fc34d630 100644 --- a/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx +++ b/benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx @@ -1,11 +1,4 @@ -import { - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' +import { useEffect, useMemo, useState } from 'react' import { dashboardTableRows, filterDashboardData, @@ -15,11 +8,15 @@ import { } from './data' import { shadcnDashboardStyles } from './styles' import type { ComponentType, ReactNode, SVGProps } from 'react' -import type { ConformanceInput, ConformanceTestDriver } from '../../types' + +export interface DashboardSize { + width: number + height: number +} export interface DashboardChartProps { data: readonly DashboardDatum[] - input: ConformanceInput + input: DashboardSize } export function dashboardChartWidth(width: number): number { @@ -72,7 +69,7 @@ export function dashboardTickValues( interface DashboardProps { ChartRenderer: ComponentType - input: ConformanceInput + input: DashboardSize } const ranges: readonly { value: DashboardRange; label: string }[] = [ @@ -136,11 +133,7 @@ const secondaryNavigation = [ ['Search', 'search'], ] as const -export const ShadcnDashboard = forwardRef< - ConformanceTestDriver, - DashboardProps ->(function ShadcnDashboard({ ChartRenderer, input }, ref) { - const rootRef = useRef(null) +export function ShadcnDashboard({ ChartRenderer, input }: DashboardProps) { const [range, setRange] = useState( input.width < 768 ? '7d' : '90d', ) @@ -150,45 +143,8 @@ export const ShadcnDashboard = forwardRef< if (input.width < 768) setRange('7d') }, [input.width]) - useImperativeHandle( - ref, - () => ({ - resolveTarget(target) { - if (target.view && target.view !== 'main') return null - const value = target.anchor.startsWith('range:') - ? target.anchor.slice('range:'.length) - : null - const element = rootRef.current?.querySelector( - `[data-range-value="${value}"]`, - ) - return element ? center(element) : null - }, - readState() { - return { - range, - chartRows: chartData.length, - tableRows: dashboardTableRows.length, - } - }, - viewBounds(view) { - if (view && view !== 'main') return null - const bounds = rootRef.current?.getBoundingClientRect() - return bounds - ? { - x: bounds.left, - y: bounds.top, - width: bounds.width, - height: bounds.height, - } - : null - }, - }), - [chartData.length, range], - ) - return (
) -}) +} function DashboardSidebar() { return ( @@ -675,12 +631,3 @@ const iconPaths: Record = { 'chevron-left': , 'chevron-right': , } - -function center(element: HTMLElement | SVGElement) { - const bounds = element.getBoundingClientRect() - return { - x: bounds.left + bounds.width / 2, - y: bounds.top + bounds.height / 2, - focusElement: element, - } -} diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack-view.tsx b/benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx similarity index 72% rename from benchmarks/conformance/cases/127-shadcn-dashboard/tanstack-view.tsx rename to benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx index d094de21..07bc9d65 100644 --- a/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack-view.tsx +++ b/benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx @@ -1,7 +1,8 @@ -import { forwardRef, useMemo } from 'react' +import { useMemo } from 'react' import { areaY, d3Curve, defineChart, stack } from '@tanstack/charts' -import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' import { Chart } from '@tanstack/charts/react/core' +import { tooltip } from '@tanstack/charts/tooltip' import { scaleLinear, scalePoint } from 'd3-scale' import { curveNatural } from 'd3-shape' import { @@ -18,12 +19,7 @@ import { type DashboardDatum, type DashboardSeries, } from './data' -import { reactMount } from '../../shared/react-mount' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' import type { ChartTooltipContent, ChartTooltipOptions } from '@tanstack/charts' -import type { ConformanceTestDriver } from '../../types' -import type { ReactConformanceProps } from '../../shared/react-mount' const seriesOrder: readonly DashboardSeries[] = ['mobile', 'desktop'] const primaryColor = 'var(--sd-primary, var(--ts-chart-1, #171717))' @@ -84,10 +80,7 @@ export function shadcnDashboardChartDefinition( tickLabels: false, }, }, - color: { - domain: seriesOrder, - range: [primaryColor, primaryColor], - }, + color: { domain: seriesOrder, range: [primaryColor, primaryColor] }, gradients: [ { id: 'fill-mobile', @@ -135,34 +128,51 @@ const dashboardTooltip: ChartTooltipOptions = { rows: points.map((point) => ({ label: point.datum.series === 'desktop' ? 'Desktop' : 'Mobile', value: point.datum.visitors.toLocaleString('en-US'), - color: 'var(--sd-primary)', + color: primaryColor, })), } }, } -function TanStackDashboardChart({ data, input }: DashboardChartProps) { +export function createExampleChart( + data: readonly DashboardDatum[], + width: number, +) { + return defineChart(shadcnDashboardChartDefinition(data, width), { + svgAnimation: false, + focus: 'group-x', + focusRing: true, + maxFocusDistance: Number.POSITIVE_INFINITY, + keyboard: true, + tooltip: { use: tooltip, ...dashboardTooltip }, + }) +} + +export const definition = createExampleChart(filterDashboardData('90d'), 288) + +function DashboardChart({ data, input }: DashboardChartProps) { const width = dashboardChartWidth(input.width) const renderer = useMemo( - () => createShadcnSpringRenderer(), - [], - ) - const definition = useMemo( () => - defineChart(shadcnDashboardChartDefinition(data, width), { - svgAnimation: false, - focus: 'group-x', - focusRing: true, - maxFocusDistance: Number.POSITIVE_INFINITY, - keyboard: true, - tooltip: { use: tooltip, ...dashboardTooltip }, + motion({ + initial: 'always', + transition: { + type: 'spring', + stiffness: 170, + damping: 18, + mass: 1, + }, }), + [], + ) + const chartDefinition = useMemo( + () => createExampleChart(data, width), [data, width], ) return ( (function TanStackDashboardView({ input }, ref) { +export interface ExampleProps { + width?: number + height?: number +} + +export default function Example({ width = 1024, height = 768 }: ExampleProps) { return ( - + ) -}) - -export const catalogCase = tanstackCase( - () => shadcnDashboardChartDefinition(filterDashboardData('90d')), - 'Total visitors for the last three months', - dashboardTooltip, - { margin: true }, -) - -export const mount = reactMount(TanStackDashboardView) +} diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/recharts-view.tsx b/benchmarks/conformance/cases/127-shadcn-dashboard/recharts-view.tsx index 97527403..f8ea5d9c 100644 --- a/benchmarks/conformance/cases/127-shadcn-dashboard/recharts-view.tsx +++ b/benchmarks/conformance/cases/127-shadcn-dashboard/recharts-view.tsx @@ -118,11 +118,7 @@ const RechartsDashboardView = forwardRef< ReactConformanceProps >(function RechartsDashboardView({ input }, ref) { return ( - + ) }) diff --git a/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack.ts b/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack.ts index 2048d598..3a7115b8 100644 --- a/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack.ts +++ b/benchmarks/conformance/cases/127-shadcn-dashboard/tanstack.ts @@ -1,5 +1,70 @@ -export { - catalogCase, - mount, - shadcnDashboardChartDefinition, -} from './tanstack-view' +import { createElement, forwardRef, useImperativeHandle, useRef } from 'react' +import Example, { createExampleChart } from './example' +import { dashboardTableRows, filterDashboardData } from './data' +import { reactMount } from '../../shared/react-mount' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceTestDriver } from '../../types' +import type { ReactConformanceProps } from '../../shared/react-mount' + +export * from './example' + +const ConformanceExample = forwardRef< + ConformanceTestDriver, + ReactConformanceProps +>(function ConformanceExample({ input }, ref) { + const rootRef = useRef(null) + useImperativeHandle(ref, () => ({ + resolveTarget(target) { + if (target.view && target.view !== 'main') return null + const value = target.anchor.startsWith('range:') + ? target.anchor.slice('range:'.length) + : null + const element = rootRef.current?.querySelector( + `[data-range-value="${value}"]`, + ) + if (!element) return null + const bounds = element.getBoundingClientRect() + return { + x: bounds.left + bounds.width / 2, + y: bounds.top + bounds.height / 2, + focusElement: element, + } + }, + readState() { + const range = + rootRef.current?.querySelector('.shadcn-dashboard') + ?.dataset.range ?? '90d' + return { + range, + chartRows: filterDashboardData(range as '90d' | '30d' | '7d').length, + tableRows: dashboardTableRows.length, + } + }, + viewBounds(view) { + if (view && view !== 'main') return null + const bounds = rootRef.current + ?.querySelector('.shadcn-dashboard') + ?.getBoundingClientRect() + return bounds + ? { + x: bounds.left, + y: bounds.top, + width: bounds.width, + height: bounds.height, + } + : null + }, + })) + return createElement( + 'div', + { ref: rootRef }, + createElement(Example, { width: input.width, height: input.height }), + ) +}) + +export const mount = reactMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => createExampleChart(filterDashboardData('90d'), 288), + 'Total visitors for the last three months', + { margin: true }, +) diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/case.json b/benchmarks/conformance/cases/128-shadcn-bar-multiple/case.json index 83ef3d45..9e39ce81 100644 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/case.json +++ b/benchmarks/conformance/cases/128-shadcn-bar-multiple/case.json @@ -5,24 +5,23 @@ "height": 600, "order": 1290, "id": "128-shadcn-bar-multiple", - "title": "shadcn bar chart – multiple", + "title": "shadcn Bar Chart - Multiple", "family": "bar", - "intent": "Reproduce shadcn/ui's grouped bar example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-bar-multiple example with TanStack Charts.", "support": "native", - "features": [ - "grouped bars", - "rounded corners", - "horizontal grid", - "grouped tooltip", - "shadcn card shell" + "features": ["bar chart", "multiple", "shadcn card shell"], + "geometry": [ + { + "role": "bar", + "count": 12 + } ], - "geometry": [{ "role": "bar", "count": 12 }], "source": { "title": "shadcn/ui chart-bar-multiple", "url": "https://ui.shadcn.com/view/new-york-v4/chart-bar-multiple" }, "ai": { "create": "Reproduce the official shadcn/ui chart-bar-multiple card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream data, grouped layout, axis treatment, tooltip labels, card dimensions, footer, and light and dark tokens." + "maintain": "Preserve the pinned upstream data, bar multiple treatment, card dimensions, and light and dark tokens." } } diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/data.ts b/benchmarks/conformance/cases/128-shadcn-bar-multiple/data.ts deleted file mode 100644 index d3c06bf5..00000000 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/data.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface VisitorMonth { - month: string - desktop: number - mobile: number -} - -export const visitorMonths: readonly VisitorMonth[] = [ - { month: 'January', desktop: 186, mobile: 80 }, - { month: 'February', desktop: 305, mobile: 200 }, - { month: 'March', desktop: 237, mobile: 120 }, - { month: 'April', desktop: 73, mobile: 190 }, - { month: 'May', desktop: 209, mobile: 130 }, - { month: 'June', desktop: 214, mobile: 140 }, -] diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx b/benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx new file mode 100644 index 00000000..7e50311d --- /dev/null +++ b/benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx @@ -0,0 +1,198 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + group, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnSeriesRows } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + return groupedBarDefinition() +} +function groupedBarDefinition() { + const rows = shadcnSeriesRows.filter( + (row) => row.series === 'desktop' || row.series === 'mobile', + ) + const groupScale = scaleBand() + .domain(twoSeries) + .paddingInner(0.08) + .paddingOuter(0) + return defineChart({ + marks: [ + barY(rows, { + id: 'visitor-bars', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: group({ scale: groupScale }), + radius: 4, + }), + ], + x: shadcnXAxis(), + y: { scale: scaleLinear, grid: true, axis: false }, + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: { top: 5, right: 5, bottom: 35, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnXAxis() { + return { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Multiple

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts-view.tsx b/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts-view.tsx deleted file mode 100644 index 4767d5a9..00000000 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts-view.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Bar, BarChart, CartesianGrid, Tooltip, XAxis } from 'recharts' -import { visitorMonths } from './data' -import { BarMultipleCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -function RechartsView({ input }: { input: ConformanceInput }) { - return ( - - {({ width, height }) => ( - - - value.slice(0, 3)} - /> - - - - - )} - - ) -} - -export const rechartsMount = shadcnChartMount(RechartsView) diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts.ts b/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts.ts index 93097581..dea60afd 100644 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts.ts +++ b/benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts.ts @@ -1 +1,5 @@ -export { rechartsMount as mount } from './recharts-view' +import { createShadcnRechartsExample } from '../../shared/shadcn-catalog-recharts' + +const example = createShadcnRechartsExample('chart-bar-multiple') + +export const mount = example.mount diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/styles.css b/benchmarks/conformance/cases/128-shadcn-bar-multiple/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/128-shadcn-bar-multiple/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack-view.tsx b/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack-view.tsx deleted file mode 100644 index 58ea93a0..00000000 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack-view.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useMemo } from 'react' -import { barY, defineChart, group } from '@tanstack/charts' -import { Chart } from '@tanstack/charts/react/core' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleBand, scaleLinear } from 'd3-scale' -import { visitorMonths, type VisitorMonth } from './data' -import { BarMultipleCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' -import type { ChartTooltipOptions } from '@tanstack/charts' -import type { ConformanceInput } from '../../types' - -const series = ['desktop', 'mobile'] as const -const colors = [ - 'var(--chart-1, var(--ts-chart-1))', - 'var(--chart-2, var(--ts-chart-2))', -] -const groupScale = scaleBand() - .domain(series) - .paddingInner(0.08) - .paddingOuter(0) - -export const barMultipleDefinition = defineChart({ - marks: [ - barY(visitorMonths, { - id: 'desktop-bars', - x: 'month', - y: 'desktop', - z: () => 'desktop', - color: () => 'desktop', - layout: group({ scale: groupScale }), - radius: 4, - }), - barY(visitorMonths, { - id: 'mobile-bars', - x: 'month', - y: 'mobile', - z: () => 'mobile', - color: () => 'mobile', - layout: group({ scale: groupScale }), - radius: 4, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), - axis: { - line: false, - ticks: { size: 0, padding: 10, format: (value) => value.slice(0, 3) }, - }, - }, - y: { - scale: scaleLinear().domain([0, 320]), - grid: true, - axis: { line: false, ticks: false, tickLabels: false }, - }, - color: { domain: series, range: colors }, - margin: { top: 5, right: 5, bottom: 35, left: 5 }, - theme: { - foreground: 'var(--muted-foreground, var(--muted))', - grid: 'var(--border)', - }, -}) - -const barTooltip: ChartTooltipOptions = { - className: 'sc-chart-tooltip', - anchor: 'group-center', - sort: 'color-domain', - content: (points) => ({ - title: points[0]?.datum.month ?? '', - rows: points.map((point) => ({ - label: point.markId === 'desktop-bars' ? 'Desktop' : 'Mobile', - value: Number(point.yValue).toLocaleString('en-US'), - color: point.markId === 'desktop-bars' ? colors[0] : colors[1], - })), - }), -} - -function TanStackView({ input }: { input: ConformanceInput }) { - const renderer = useMemo( - () => createShadcnSpringRenderer(), - [], - ) - const definition = useMemo( - () => - defineChart(barMultipleDefinition, { - svgAnimation: false, - focus: 'group-x', - keyboard: true, - tooltip: { use: tooltip, ...barTooltip }, - }), - [], - ) - return ( - - {({ width, height }) => ( - - )} - - ) -} - -export const catalogCase = tanstackCase( - () => barMultipleDefinition, - 'Monthly desktop and mobile visitors', - barTooltip, - { margin: true }, -) -export const tanstackMount = shadcnChartMount(TanStackView) diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack.ts b/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack.ts index d61b9f66..f1475c5f 100644 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack.ts +++ b/benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack.ts @@ -1,5 +1,18 @@ -export { - barMultipleDefinition, - catalogCase, - tanstackMount as mount, -} from './tanstack-view' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +export const barMultipleDefinition = definition +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Multiple implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/128-shadcn-bar-multiple/view.tsx b/benchmarks/conformance/cases/128-shadcn-bar-multiple/view.tsx deleted file mode 100644 index 576c29e9..00000000 --- a/benchmarks/conformance/cases/128-shadcn-bar-multiple/view.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { - ShadcnChartCard, - ShadcnTrendFooter, -} from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -export function BarMultipleCard({ - input, - children, -}: { - input: ConformanceInput - children: Parameters[0]['children'] -}) { - return ( - } - > - {children} - - ) -} diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/case.json b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/case.json index 0f23420a..8e93cdbb 100644 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/case.json +++ b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/case.json @@ -5,20 +5,16 @@ "height": 600, "order": 1300, "id": "129-shadcn-pie-donut-text", - "title": "shadcn pie chart – donut with text", + "title": "shadcn Pie Chart - Donut with Text", "family": "pie", - "intent": "Reproduce shadcn/ui's donut-with-center-text example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-pie-donut-text example with TanStack Charts.", "support": "native", - "features": [ - "donut chart", - "center text", - "sector separators", - "tooltip", - "shadcn card shell" - ], + "features": ["pie chart", "donut text", "shadcn card shell"], "geometry": [ - { "role": "arc", "count": 5 }, - { "role": "text", "count": 2 } + { + "role": "arc", + "count": 5 + } ], "source": { "title": "shadcn/ui chart-pie-donut-text", @@ -26,6 +22,6 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-pie-donut-text card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream browser data, donut radii, separators, center total and label, tooltip, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, pie donut text treatment, card dimensions, and light and dark tokens." } } diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/data.ts b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/data.ts deleted file mode 100644 index a4dfca5b..00000000 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/data.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface BrowserDatum { - browser: string - visitors: number -} - -export interface CenterLabel { - id: string - angle: number - radius: number - text: string -} - -export const browserData: readonly BrowserDatum[] = [ - { browser: 'chrome', visitors: 275 }, - { browser: 'safari', visitors: 200 }, - { browser: 'firefox', visitors: 287 }, - { browser: 'edge', visitors: 173 }, - { browser: 'other', visitors: 190 }, -] - -export const browserColors = [ - 'var(--chart-1, var(--ts-chart-1))', - 'var(--chart-2, var(--ts-chart-2))', - 'var(--chart-3, var(--ts-chart-3))', - 'var(--chart-4, var(--ts-chart-4))', - 'var(--chart-5, var(--ts-chart-5))', -] - -export const totalVisitors = browserData.reduce( - (sum, row) => sum + row.visitors, - 0, -) - -export const centerLabels: readonly CenterLabel[] = [ - { - id: 'total', - angle: 0, - radius: 0, - text: totalVisitors.toLocaleString('en-US'), - }, - { id: 'label', angle: 0, radius: 0, text: 'Visitors' }, -] diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx new file mode 100644 index 00000000..0292e522 --- /dev/null +++ b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx @@ -0,0 +1,213 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: 60, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + radialText([{ id: 'total', angle: 0, radius: 0, text: '1,125' }], { + id: 'visitor-total', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: -5, + fill: 'var(--foreground)', + fontSize: 30, + fontWeight: 700, + }), + radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], { + id: 'visitor-label', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: 19, + fill: 'var(--muted-foreground)', + fontSize: 14, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Donut with Text

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts-view.tsx b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts-view.tsx deleted file mode 100644 index ef9d87d7..00000000 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts-view.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Cell, Pie, PieChart, Tooltip } from 'recharts' -import { browserColors, browserData, totalVisitors } from './data' -import { PieDonutTextCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -function RechartsView({ input }: { input: ConformanceInput }) { - return ( - - {({ width, height }) => ( - - - - {browserData.map((row, index) => ( - - ))} - - - {totalVisitors.toLocaleString('en-US')} - - - Visitors - - - )} - - ) -} - -export const rechartsMount = shadcnChartMount(RechartsView) diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts.ts b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts.ts index 93097581..798d6493 100644 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts.ts +++ b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts.ts @@ -1 +1,5 @@ -export { rechartsMount as mount } from './recharts-view' +import { createShadcnRechartsExample } from '../../shared/shadcn-catalog-recharts' + +const example = createShadcnRechartsExample('chart-pie-donut-text') + +export const mount = example.mount diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/styles.css b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack-view.tsx b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack-view.tsx deleted file mode 100644 index c4f8c350..00000000 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack-view.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useMemo } from 'react' -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc, radialText } from '@tanstack/charts/polar' -import { Chart } from '@tanstack/charts/react/core' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleLinear } from 'd3-scale' -import { - browserColors, - browserData, - centerLabels, - type BrowserDatum, - type CenterLabel, -} from './data' -import { PieDonutTextCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' -import type { ChartTooltipOptions } from '@tanstack/charts' -import type { PieDatum } from '@tanstack/charts/polar' -import type { ConformanceInput } from '../../types' - -const browsers = browserData.map((row) => row.browser) -const arcs = pie(browserData, { - value: 'visitors', - startAngle: Math.PI / 2, - endAngle: (-Math.PI * 3) / 2, -}) - -export const pieDonutTextDefinition = defineChart({ - marks: [ - polar({ - radiusRatio: 0.768, - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialArc(arcs, { - id: 'browser-slices', - key: 'browser', - innerRadius: ({ radius }) => radius * 0.625, - color: 'browser', - stroke: 'var(--background, var(--panel))', - strokeWidth: 5, - }), - radialText(centerLabels.slice(0, 1), { - id: 'visitor-total', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: -5, - fill: 'var(--foreground, currentColor)', - fontSize: 30, - fontWeight: 700, - }), - radialText(centerLabels.slice(1), { - id: 'visitor-label', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: 19, - fill: 'var(--muted-foreground, var(--muted))', - fontSize: 14, - }), - ], - }), - ], - color: { domain: browsers, range: browserColors }, - margin: 0, -}) - -const donutTooltip: ChartTooltipOptions | CenterLabel> = - { - className: 'sc-chart-tooltip', - content: (points) => ({ - rows: points.flatMap((point) => - 'browser' in point.datum - ? [ - { - label: - point.datum.browser[0]!.toUpperCase() + - point.datum.browser.slice(1), - value: point.datum.visitors.toLocaleString('en-US'), - color: point.color, - }, - ] - : [], - ), - }), - } - -function TanStackView({ input }: { input: ConformanceInput }) { - const renderer = useMemo( - () => - createShadcnSpringRenderer< - PieDatum | CenterLabel, - number, - number - >(), - [], - ) - const definition = useMemo( - () => - defineChart(pieDonutTextDefinition, { - svgAnimation: false, - keyboard: true, - tooltip: { use: tooltip, ...donutTooltip }, - }), - [], - ) - return ( - - {({ width, height }) => ( - - )} - - ) -} - -export const catalogCase = tanstackCase( - () => pieDonutTextDefinition, - 'Browser visitors donut', - donutTooltip, -) -export const tanstackMount = shadcnChartMount(TanStackView) diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack.ts b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack.ts index 07b01682..e9c7f828 100644 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack.ts +++ b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack.ts @@ -1,5 +1,18 @@ -export { - catalogCase, - pieDonutTextDefinition, - tanstackMount as mount, -} from './tanstack-view' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +export const pieDonutTextDefinition = definition +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Donut with Text implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/view.tsx b/benchmarks/conformance/cases/129-shadcn-pie-donut-text/view.tsx deleted file mode 100644 index a999fcf7..00000000 --- a/benchmarks/conformance/cases/129-shadcn-pie-donut-text/view.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - ShadcnChartCard, - ShadcnTrendFooter, -} from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -export function PieDonutTextCard({ - input, - children, -}: { - input: ConformanceInput - children: Parameters[0]['children'] -}) { - return ( - } - > - {children} - - ) -} diff --git a/benchmarks/conformance/cases/13-interval-timeline/example.tsx b/benchmarks/conformance/cases/13-interval-timeline/example.tsx new file mode 100644 index 00000000..dd4aed0f --- /dev/null +++ b/benchmarks/conformance/cases/13-interval-timeline/example.tsx @@ -0,0 +1,70 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { aapl } from '@charts-poc/demo-data/aapl' +import { barX, colorLegend, defineChart } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +const colors = ['#10b981', '#ef4444'] +const date = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', +}) + +export const definition = (input: ExampleOptions) => { + const rows = aapl.slice(input.revision * 3, input.revision * 3 + 8) + + return defineChart({ + marks: [ + barX(rows, { + x1: 'Open', + x2: 'Close', + y: 'Date', + color: (row) => (row.Close >= row.Open ? 'Gain' : 'Loss'), + inset: 1, + radius: 3, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Share price ($)' } }, + y: { + scale: () => scaleBand().paddingInner(0.16), + axis: { ticks: { format: (value) => date.format(value) } }, + }, + color: { + range: colors, + legend: colorLegend({ label: 'Session' }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Apple daily open-to-close price ranges' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `${date.format(point.datum.Date)} · Open $${point.datum.Open.toFixed(2)} · Close $${point.datum.Close.toFixed(2)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts b/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts index 3028c2a3..6c029e65 100644 --- a/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts +++ b/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts @@ -1,49 +1,8 @@ -import { aapl } from '@charts-poc/demo-data/aapl' -import { barX, colorLegend, defineChart } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#10b981', '#ef4444'] -const date = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - timeZone: 'UTC', -}) +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = aapl.slice(input.revision * 3, input.revision * 3 + 8) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - barX(rows, { - x1: 'Open', - x2: 'Close', - y: 'Date', - color: (row) => (row.Close >= row.Open ? 'Gain' : 'Loss'), - inset: 1, - radius: 3, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Share price ($)' } }, - y: { - scale: () => scaleBand().paddingInner(0.16), - axis: { ticks: { format: (value) => date.format(value) } }, - }, - color: { - range: colors, - legend: colorLegend({ label: 'Session' }), - }, - }) -} - -export const catalogCase = tanstackCase( - definition, - 'Apple daily open-to-close price ranges', - { - format: (point) => - `${date.format(point.datum.Date)} · Open $${point.datum.Open.toFixed(2)} · Close $${point.datum.Close.toFixed(2)}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/case.json b/benchmarks/conformance/cases/130-shadcn-radar-multiple/case.json index 38236c2b..db3ecb39 100644 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/case.json +++ b/benchmarks/conformance/cases/130-shadcn-radar-multiple/case.json @@ -5,25 +5,23 @@ "height": 600, "order": 1310, "id": "130-shadcn-radar-multiple", - "title": "shadcn radar chart – multiple", + "title": "shadcn Radar Chart - Multiple", "family": "radar", - "intent": "Reproduce shadcn/ui's multi-series radar example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-radar-multiple example with TanStack Charts.", "support": "native", - "features": [ - "radar chart", - "multiple series", - "polygon grid", - "angle labels", - "grouped tooltip", - "shadcn card shell" + "features": ["radar chart", "multiple", "shadcn card shell"], + "geometry": [ + { + "role": "radar", + "count": 2 + } ], - "geometry": [{ "role": "radar", "count": 2 }], "source": { "title": "shadcn/ui chart-radar-multiple", "url": "https://ui.shadcn.com/view/new-york-v4/chart-radar-multiple" }, "ai": { "create": "Reproduce the official shadcn/ui chart-radar-multiple card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream data, polygon guide, angle labels, two filled profiles, tooltip, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, radar multiple treatment, card dimensions, and light and dark tokens." } } diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/data.ts b/benchmarks/conformance/cases/130-shadcn-radar-multiple/data.ts deleted file mode 100644 index 709505d3..00000000 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/data.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface RadarDatum { - month: string - desktop: number - mobile: number -} - -export interface RadarPoint { - month: string - series: 'desktop' | 'mobile' - visitors: number -} - -export const radarData: readonly RadarDatum[] = [ - { month: 'January', desktop: 186, mobile: 80 }, - { month: 'February', desktop: 305, mobile: 200 }, - { month: 'March', desktop: 237, mobile: 120 }, - { month: 'April', desktop: 73, mobile: 190 }, - { month: 'May', desktop: 209, mobile: 130 }, - { month: 'June', desktop: 214, mobile: 140 }, -] - -export const radarMonths = radarData.map((row) => row.month) -export const radarSeries = ['desktop', 'mobile'] as const -export const radarColors = [ - 'var(--chart-1, var(--ts-chart-1))', - 'var(--chart-2, var(--ts-chart-2))', -] as const -export const radarPoints: readonly RadarPoint[] = radarData.flatMap((row) => [ - { month: row.month, series: 'desktop', visitors: row.desktop }, - { month: row.month, series: 'mobile', visitors: row.mobile }, -]) diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx b/benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx new file mode 100644 index 00000000..e73ec961 --- /dev/null +++ b/benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx @@ -0,0 +1,223 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarMultiple } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarMultiple + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 1, + stroke: shadcnColors[1], + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Multiple

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts-view.tsx b/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts-view.tsx deleted file mode 100644 index 29305f17..00000000 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts-view.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { PolarAngleAxis, PolarGrid, Radar, RadarChart, Tooltip } from 'recharts' -import { radarData } from './data' -import { RadarMultipleCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -function RechartsView({ input }: { input: ConformanceInput }) { - const compactLabels = input.width < 350 - return ( - - {({ width, height }) => ( - - - value.slice(0, 3) : undefined - } - /> - - - - - )} - - ) -} - -export const rechartsMount = shadcnChartMount(RechartsView) diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts.ts b/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts.ts index 93097581..8246b023 100644 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts.ts +++ b/benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts.ts @@ -1 +1,5 @@ -export { rechartsMount as mount } from './recharts-view' +import { createShadcnRechartsExample } from '../../shared/shadcn-catalog-recharts' + +const example = createShadcnRechartsExample('chart-radar-multiple') + +export const mount = example.mount diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/styles.css b/benchmarks/conformance/cases/130-shadcn-radar-multiple/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/130-shadcn-radar-multiple/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack-view.tsx b/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack-view.tsx deleted file mode 100644 index 8c492aab..00000000 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack-view.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useMemo } from 'react' -import { defineChart } from '@tanstack/charts' -import { - angleGrid, - focusGroupAngle, - polar, - radialArea, - radialGrid, -} from '@tanstack/charts/polar' -import { Chart } from '@tanstack/charts/react/core' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleLinear, scalePoint } from 'd3-scale' -import { curveLinearClosed } from 'd3-shape' -import { - radarColors, - radarMonths, - radarPoints, - radarSeries, - type RadarPoint, -} from './data' -import { RadarMultipleCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' -import type { ChartTooltipOptions } from '@tanstack/charts' -import type { ConformanceInput } from '../../types' - -export const radarMultipleDefinition = defineChart({ - marks: [ - polar({ - radiusRatio: 0.76, - angle: { scale: scalePoint().domain(radarMonths), wrap: true }, - radius: { scale: scaleLinear().domain([0, 320]) }, - guides: [ - radialGrid({ - values: [80, 160, 240, 320], - shape: 'polygon', - stroke: 'var(--border)', - }), - angleGrid({ - values: radarMonths, - labels: true, - labelOffset: 10, - labelFill: 'var(--muted-foreground, var(--muted))', - labelFontSize: 12, - stroke: 'var(--border)', - }), - ], - marks: [ - radialArea( - radarPoints.filter((row) => row.series === 'desktop'), - { - id: 'desktop-radar', - className: 'ts-chart__radar', - angle: 'month', - radius: 'visitors', - key: 'month', - z: 'series', - curve: curveLinearClosed, - fill: radarColors[0], - fillOpacity: 0.6, - stroke: radarColors[0], - strokeWidth: 1, - }, - ), - radialArea( - radarPoints.filter((row) => row.series === 'mobile'), - { - id: 'mobile-radar', - className: 'ts-chart__radar', - angle: 'month', - radius: 'visitors', - key: 'month', - z: 'series', - curve: curveLinearClosed, - fill: radarColors[1], - fillOpacity: 1, - stroke: radarColors[1], - strokeWidth: 1, - }, - ), - ], - }), - ], - color: { domain: radarSeries, range: radarColors }, - margin: 0, -}) - -const radarTooltip: ChartTooltipOptions = { - className: 'sc-chart-tooltip', - anchor: 'group-center', - content: (focused) => ({ - title: focused[0]?.datum.month ?? '', - rows: focused.map((point) => ({ - label: point.datum.series === 'desktop' ? 'Desktop' : 'Mobile', - value: point.datum.visitors.toLocaleString('en-US'), - color: point.datum.series === 'desktop' ? radarColors[0] : radarColors[1], - })), - }), -} - -function TanStackView({ input }: { input: ConformanceInput }) { - const renderer = useMemo( - () => createShadcnSpringRenderer(), - [], - ) - const definition = useMemo( - () => - defineChart(radarMultipleDefinition, { - svgAnimation: false, - focus: focusGroupAngle, - keyboard: true, - tooltip: { use: tooltip, ...radarTooltip }, - }), - [], - ) - return ( - - {({ width, height }) => ( - - )} - - ) -} - -export const catalogCase = tanstackCase( - () => radarMultipleDefinition, - 'Desktop and mobile visitor radar', - radarTooltip, - { guides: true }, -) -export const tanstackMount = shadcnChartMount(TanStackView) diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack.ts b/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack.ts index b786131e..2c14f3b5 100644 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack.ts +++ b/benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack.ts @@ -1,5 +1,18 @@ -export { - catalogCase, - radarMultipleDefinition, - tanstackMount as mount, -} from './tanstack-view' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +export const radarMultipleDefinition = definition +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Multiple implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/130-shadcn-radar-multiple/view.tsx b/benchmarks/conformance/cases/130-shadcn-radar-multiple/view.tsx deleted file mode 100644 index 86b793fd..00000000 --- a/benchmarks/conformance/cases/130-shadcn-radar-multiple/view.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { - ShadcnChartCard, - ShadcnTrendFooter, -} from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -export function RadarMultipleCard({ - input, - children, -}: { - input: ConformanceInput - children: Parameters[0]['children'] -}) { - return ( - = 400 ? 16 : 0} - footer={} - > - {children} - - ) -} diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/case.json b/benchmarks/conformance/cases/131-shadcn-radial-text/case.json index 6c5986a1..2e6b199e 100644 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/case.json +++ b/benchmarks/conformance/cases/131-shadcn-radial-text/case.json @@ -5,20 +5,20 @@ "height": 600, "order": 1320, "id": "131-shadcn-radial-text", - "title": "shadcn radial chart – text", + "title": "shadcn Radial Chart - Text", "family": "radial", - "intent": "Reproduce shadcn/ui's partial radial chart with center text while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-radial-text example with TanStack Charts.", "support": "native", - "features": [ - "partial radial bar", - "rounded caps", - "background track", - "center text", - "shadcn card shell" - ], + "features": ["radial chart", "text", "shadcn card shell"], "geometry": [ - { "role": "arc", "count": 2 }, - { "role": "text", "count": 2 } + { + "role": "bar", + "count": 1, + "rendererRoles": { + "recharts": "arc", + "tanstack": "arc" + } + } ], "source": { "title": "shadcn/ui chart-radial-text", @@ -26,6 +26,6 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-radial-text card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream value, 250-degree sweep, 80/90 radii, rounded track, center text, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, radial text treatment, card dimensions, and light and dark tokens." } } diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/data.ts b/benchmarks/conformance/cases/131-shadcn-radial-text/data.ts deleted file mode 100644 index b387bee9..00000000 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/data.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface RadialDatum { - browser: 'safari' - visitors: number - ring: 'visitors' -} - -export const radialData: readonly RadialDatum[] = [ - { browser: 'safari', visitors: 200, ring: 'visitors' }, -] - -export const radialCenterLabels = [ - { id: 'value', angle: 0, radius: 0, text: '200' }, - { id: 'label', angle: 0, radius: 0, text: 'Visitors' }, -] diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx b/benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx new file mode 100644 index 00000000..90e4e3e7 --- /dev/null +++ b/benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx @@ -0,0 +1,264 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialBarAngle, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + return buildSingleRadialDefinition({ + id: 'text', + visitors: 200, + endDegrees: 250, + innerRadius: 80, + outerRadius: 90, + backgroundInnerRadius: 80, + backgroundOuterRadius: 90, + cornerRadius: 10, + total: '200', + }) +} +function buildSingleRadialDefinition(options: { + id: string + visitors: number + endDegrees: number + innerRadius: number + outerRadius: number + backgroundInnerRadius: number + backgroundOuterRadius: number + cornerRadius: number + total: string +}) { + const rows = [ + { browser: 'safari', visitors: options.visitors, ring: 'visitors' }, + ] + const background = pie([{ id: 'background', value: 1 }], { value: 'value' }) + return defineChart({ + marks: [ + polar({ + marks: [ + radialArc(background, { + id: `${options.id}-background`, + key: 'id', + innerRadius: options.backgroundInnerRadius, + outerRadius: options.backgroundOuterRadius, + fill: 'var(--muted)', + }), + ], + }), + polar({ + startAngle: rechartsPolarAngle(0), + endAngle: rechartsPolarAngle(options.endDegrees), + angle: { scale: scaleLinear().domain([0, options.visitors]) }, + radius: { + scale: scaleBand().domain(['visitors']), + range: [options.innerRadius, options.outerRadius], + }, + marks: [ + radialBarAngle(rows, { + id: `${options.id}-value`, + angle: 'visitors', + radius: 'ring', + key: 'browser', + fill: shadcnColors[1], + cornerRadius: options.cornerRadius, + }), + ], + }), + radialCenterLabels(options.total, 36, 0, 24), + ], + margin: 0, + }) +} +function radialCenterLabels( + total: string, + fontSize: number, + totalDy: number, + labelDy: number, +) { + return polar({ + angle: { scale: scaleLinear().domain([0, 1]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialText([{ id: 'total', angle: 0, radius: 0, text: total }], { + id: `radial-total-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: totalDy, + fill: 'var(--foreground)', + fontSize, + fontWeight: 700, + }), + radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], { + id: `radial-label-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: labelDy, + fill: 'var(--muted-foreground)', + fontSize: 14, + }), + ], + }) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart - Text

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/recharts-view.tsx b/benchmarks/conformance/cases/131-shadcn-radial-text/recharts-view.tsx deleted file mode 100644 index dcda5380..00000000 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/recharts-view.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { PolarGrid, RadialBar, RadialBarChart } from 'recharts' -import { radialData } from './data' -import { RadialTextCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -function RechartsView({ input }: { input: ConformanceInput }) { - return ( - - {({ width, height }) => ( - - - - - 200 - - - Visitors - - - )} - - ) -} - -export const rechartsMount = shadcnChartMount(RechartsView) diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/recharts.ts b/benchmarks/conformance/cases/131-shadcn-radial-text/recharts.ts index 93097581..4cd43e5a 100644 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/recharts.ts +++ b/benchmarks/conformance/cases/131-shadcn-radial-text/recharts.ts @@ -1 +1,5 @@ -export { rechartsMount as mount } from './recharts-view' +import { createShadcnRechartsExample } from '../../shared/shadcn-catalog-recharts' + +const example = createShadcnRechartsExample('chart-radial-text') + +export const mount = example.mount diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/styles.css b/benchmarks/conformance/cases/131-shadcn-radial-text/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/131-shadcn-radial-text/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack-view.tsx b/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack-view.tsx deleted file mode 100644 index 02448aea..00000000 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack-view.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useMemo } from 'react' -import { defineChart } from '@tanstack/charts' -import { - pie, - polar, - radialArc, - radialBarAngle, - radialText, -} from '@tanstack/charts/polar' -import { Chart } from '@tanstack/charts/react/core' -import { scaleBand, scaleLinear } from 'd3-scale' -import { radialCenterLabels, radialData, type RadialDatum } from './data' -import { RadialTextCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' -import type { ConformanceInput } from '../../types' -import type { PieDatum } from '@tanstack/charts/polar' - -const startAngle = Math.PI / 2 -const endAngle = (-Math.PI * 160) / 180 -const radiusScale = scaleBand().domain(['visitors']) -const backgroundArc = pie([{ id: 'background', value: 1 }], { value: 'value' }) -type RadialTextDatum = - | PieDatum<{ id: string; value: number }> - | RadialDatum - | (typeof radialCenterLabels)[number] - -export const radialTextDefinition = defineChart({ - marks: [ - polar({ - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialArc(backgroundArc, { - id: 'radial-background', - key: 'id', - innerRadius: 80, - outerRadius: 90, - fill: 'var(--muted, var(--panel-muted))', - }), - ], - }), - polar({ - radiusRatio: 0.72, - startAngle, - endAngle, - angle: { scale: scaleLinear().domain([0, 200]) }, - radius: { - scale: radiusScale, - range: [80, 90], - }, - marks: [ - radialBarAngle(radialData, { - id: 'radial-value', - angle: 'visitors', - radius: 'ring', - key: 'browser', - fill: 'var(--chart-2, var(--ts-chart-2))', - cornerRadius: 10, - }), - ], - }), - polar({ - angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialText(radialCenterLabels.slice(0, 1), { - id: 'radial-total', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: 0, - fill: 'var(--foreground, currentColor)', - fontSize: 36, - fontWeight: 700, - }), - radialText(radialCenterLabels.slice(1), { - id: 'radial-label', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - dy: 24, - fill: 'var(--muted-foreground, var(--muted))', - fontSize: 14, - }), - ], - }), - ], - margin: 0, -}) - -function TanStackView({ input }: { input: ConformanceInput }) { - const renderer = useMemo( - () => - createShadcnSpringRenderer(), - [], - ) - const definition = useMemo( - () => - defineChart(radialTextDefinition, { - svgAnimation: false, - focus: 'nearest', - }), - [], - ) - return ( - - {({ width, height }) => ( - - )} - - ) -} - -export const catalogCase = tanstackCase( - () => radialTextDefinition, - 'Safari visitors radial chart', -) -export const tanstackMount = shadcnChartMount(TanStackView) diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack.ts b/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack.ts index 452af0bf..1fe4bc1c 100644 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack.ts +++ b/benchmarks/conformance/cases/131-shadcn-radial-text/tanstack.ts @@ -1,5 +1,18 @@ -export { - catalogCase, - radialTextDefinition, - tanstackMount as mount, -} from './tanstack-view' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +export const radialTextDefinition = definition +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart - Text implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/131-shadcn-radial-text/view.tsx b/benchmarks/conformance/cases/131-shadcn-radial-text/view.tsx deleted file mode 100644 index ac23e919..00000000 --- a/benchmarks/conformance/cases/131-shadcn-radial-text/view.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - ShadcnChartCard, - ShadcnTrendFooter, -} from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -export function RadialTextCard({ - input, - children, -}: { - input: ConformanceInput - children: Parameters[0]['children'] -}) { - return ( - } - > - {children} - - ) -} diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/case.json b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/case.json index 78fae304..fab10218 100644 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/case.json +++ b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/case.json @@ -5,25 +5,23 @@ "height": 600, "order": 1330, "id": "132-shadcn-tooltip-advanced", - "title": "shadcn tooltip – advanced", + "title": "shadcn Tooltip - Advanced", "family": "tooltip", - "intent": "Reproduce shadcn/ui's stacked-bar card and custom total tooltip while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-tooltip-advanced example with TanStack Charts.", "support": "native", - "features": [ - "stacked bars", - "rounded corners", - "rich tooltip body", - "computed total", - "default focus", - "shadcn card shell" + "features": ["tooltip chart", "advanced", "shadcn card shell"], + "geometry": [ + { + "role": "bar", + "count": 10 + } ], - "geometry": [{ "role": "bar", "count": 12 }], "source": { "title": "shadcn/ui chart-tooltip-advanced", "url": "https://ui.shadcn.com/view/new-york-v4/chart-tooltip-advanced" }, "ai": { - "create": "Reproduce the official shadcn/ui chart-tooltip-advanced card and data with TanStack Charts and its React tooltip composition entry.", - "maintain": "Preserve the pinned upstream data, stack order, weekday axis, initial focused day, 180-pixel rich tooltip, row labels, kcal units, total, card dimensions, and themes." + "create": "Reproduce the official shadcn/ui chart-tooltip-advanced card and data with TanStack Charts.", + "maintain": "Preserve the pinned upstream data, tooltip advanced treatment, card dimensions, and light and dark tokens." } } diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/data.ts b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/data.ts deleted file mode 100644 index 297cb656..00000000 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/data.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface ActivityDay { - date: string - running: number - swimming: number -} - -export interface ActivityPoint { - date: string - activity: 'running' | 'swimming' - calories: number -} - -export const activityData: readonly ActivityDay[] = [ - { date: '2024-07-15', running: 450, swimming: 300 }, - { date: '2024-07-16', running: 380, swimming: 420 }, - { date: '2024-07-17', running: 520, swimming: 120 }, - { date: '2024-07-18', running: 140, swimming: 550 }, - { date: '2024-07-19', running: 600, swimming: 350 }, - { date: '2024-07-20', running: 480, swimming: 400 }, -] - -export const activities = ['running', 'swimming'] as const -export const activityColors = [ - 'var(--chart-1, var(--ts-chart-1))', - 'var(--chart-2, var(--ts-chart-2))', -] as const -export const activityPoints: readonly ActivityPoint[] = activityData.flatMap( - (row) => [ - { date: row.date, activity: 'running', calories: row.running }, - { date: row.date, activity: 'swimming', calories: row.swimming }, - ], -) -export const weekday = new Intl.DateTimeFormat('en-US', { weekday: 'short' }) diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx new file mode 100644 index 00000000..4d58e376 --- /dev/null +++ b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Advanced

+

Tooltip with custom formatter and total.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="advanced" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts-view.tsx b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts-view.tsx deleted file mode 100644 index 65d38f80..00000000 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts-view.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { Bar, BarChart, Tooltip, XAxis } from 'recharts' -import { activityData, weekday, type ActivityDay } from './data' -import { AdvancedTooltipCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -function RechartsTooltipBody({ - active, - payload, -}: { - active?: boolean - payload?: readonly { - dataKey?: string - value?: number - payload?: ActivityDay - }[] -}) { - if (!active || !payload?.length) return null - const rows = payload.filter( - (entry) => entry.dataKey === 'running' || entry.dataKey === 'swimming', - ) - const total = rows.reduce((sum, entry) => sum + Number(entry.value ?? 0), 0) - return ( -
- {rows.map((entry) => ( -
- - {entry.dataKey === 'running' ? 'Running' : 'Swimming'} - - {entry.value} - kcal - -
- ))} -
- Total - - {total} - kcal - -
-
- ) -} - -function RechartsView({ input }: { input: ConformanceInput }) { - return ( - - {({ width, height }) => ( - - weekday.format(new Date(value))} - /> - - - } - /> - - )} - - ) -} - -export const rechartsMount = shadcnChartMount(RechartsView) diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts.ts b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts.ts index 93097581..86d99d14 100644 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts.ts +++ b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts.ts @@ -1 +1,5 @@ -export { rechartsMount as mount } from './recharts-view' +import { createShadcnRechartsExample } from '../../shared/shadcn-catalog-recharts' + +const example = createShadcnRechartsExample('chart-tooltip-advanced') + +export const mount = example.mount diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/styles.css b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack-view.tsx b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack-view.tsx deleted file mode 100644 index 7b8fc400..00000000 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack-view.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { useMemo, useRef } from 'react' -import { barY, defineChart, stack } from '@tanstack/charts' -import { RendererChart as TooltipChart } from '@tanstack/charts/react/tooltip' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleBand, scaleLinear } from 'd3-scale' -import { - activities, - activityColors, - activityPoints, - weekday, - type ActivityPoint, -} from './data' -import { AdvancedTooltipCard } from './view' -import { shadcnChartMount } from '../../shared/shadcn-chart-card' -import { tanstackCase } from '../../shared/mount' -import { createShadcnSpringRenderer } from '../../shared/shadcn-motion' -import type { ChartPoint, ChartTooltipOptions } from '@tanstack/charts' -import type { ConformanceInput } from '../../types' - -export const advancedTooltipDefinition = defineChart({ - marks: [ - barY(activityPoints, { - id: 'activity-bars', - x: 'date', - y: 'calories', - z: 'activity', - color: 'activity', - key: (row) => `${row.date}:${row.activity}`, - layout: stack({ order: activities }), - radius: 4, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), - axis: { - line: false, - ticks: { - size: 0, - padding: 10, - format: (value) => weekday.format(new Date(value)), - }, - }, - }, - y: { scale: scaleLinear().domain([0, 1_000]), axis: false }, - color: { domain: activities, range: activityColors }, - margin: { top: 0, right: 7, bottom: 32, left: 7 }, - theme: { foreground: 'var(--muted-foreground, var(--muted))' }, -}) - -const activityTooltip: ChartTooltipOptions = { - className: 'sc-chart-tooltip', - anchor: (_points, context) => ({ - x: context.surface.width * 0.271, - y: context.surface.height * 0.554, - }), - placement: 'bottom-right', - offset: 0, - sort: 'color-domain', - content: () => ({ rows: [] }), -} - -function AdvancedTooltipBody({ - points, -}: { - points: readonly ChartPoint[] -}) { - const ordered = [...points].sort( - (left, right) => - activities.indexOf(left.datum.activity) - - activities.indexOf(right.datum.activity), - ) - const total = ordered.reduce((sum, point) => sum + point.datum.calories, 0) - return ( -
- {ordered.map((point) => ( -
- - - {point.datum.activity === 'running' ? 'Running' : 'Swimming'} - - - {point.datum.calories} - kcal - -
- ))} -
- Total - - {total} - kcal - -
-
- ) -} - -function TanStackView({ input }: { input: ConformanceInput }) { - const focused = useRef(false) - const renderer = useMemo( - () => createShadcnSpringRenderer(), - [], - ) - const definition = useMemo( - () => - defineChart(advancedTooltipDefinition, { - svgAnimation: false, - focus: 'group-x', - keyboard: true, - tooltip: { use: tooltip, ...activityTooltip }, - }), - [], - ) - return ( - - {({ width, height }) => ( - { - if (focused.current) return - const point = scene.points.find( - (candidate) => candidate.datum.date === '2024-07-16', - ) - if (!point) return - focused.current = true - interaction.setControlledFocus(point, { source: 'programmatic' }) - }} - renderTooltipBody={({ points }) => ( - - )} - /> - )} - - ) -} - -export const catalogCase = tanstackCase( - () => advancedTooltipDefinition, - 'Daily running and swimming calories', - activityTooltip, - { margin: true }, -) -export const tanstackMount = shadcnChartMount(TanStackView) diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack.ts b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack.ts index ebb5b8c5..3beb7f8c 100644 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack.ts +++ b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack.ts @@ -1,5 +1,18 @@ -export { - advancedTooltipDefinition, - catalogCase, - tanstackMount as mount, -} from './tanstack-view' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +export const advancedTooltipDefinition = definition +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Advanced implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/view.tsx b/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/view.tsx deleted file mode 100644 index d6dd3c09..00000000 --- a/benchmarks/conformance/cases/132-shadcn-tooltip-advanced/view.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ShadcnChartCard } from '../../shared/shadcn-chart-card' -import type { ConformanceInput } from '../../types' - -export function AdvancedTooltipCard({ - input, - children, -}: { - input: ConformanceInput - children: Parameters[0]['children'] -}) { - return ( - - {children} - - ) -} diff --git a/benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx b/benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx new file mode 100644 index 00000000..00521ba0 --- /dev/null +++ b/benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx @@ -0,0 +1,210 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['mobile', 'desktop'], + ...{}, + }), + curve: d3Curve(curve), + fill: undefined, + fillOpacity: 0.4, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 600]), + grid: true, + axis: { line: false, ticks: { size: 0, values: [0, 300, 600] } }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 12, right: 12, bottom: 28, left: 40 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Axes

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/133-shadcn-area-axes/styles.css b/benchmarks/conformance/cases/133-shadcn-area-axes/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/133-shadcn-area-axes/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/133-shadcn-area-axes/tanstack.ts b/benchmarks/conformance/cases/133-shadcn-area-axes/tanstack.ts index c31d786c..05668287 100644 --- a/benchmarks/conformance/cases/133-shadcn-area-axes/tanstack.ts +++ b/benchmarks/conformance/cases/133-shadcn-area-axes/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-axes') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Axes implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/134-shadcn-area-default/example.tsx b/benchmarks/conformance/cases/134-shadcn-area-default/example.tsx new file mode 100644 index 00000000..259bfb66 --- /dev/null +++ b/benchmarks/conformance/cases/134-shadcn-area-default/example.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-area', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + fill: shadcnColors[0], + fillOpacity: 0.4, + stroke: shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/134-shadcn-area-default/styles.css b/benchmarks/conformance/cases/134-shadcn-area-default/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/134-shadcn-area-default/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/134-shadcn-area-default/tanstack.ts b/benchmarks/conformance/cases/134-shadcn-area-default/tanstack.ts index dcccf27c..44b41a4d 100644 --- a/benchmarks/conformance/cases/134-shadcn-area-default/tanstack.ts +++ b/benchmarks/conformance/cases/134-shadcn-area-default/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-default') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx b/benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx new file mode 100644 index 00000000..ef569b82 --- /dev/null +++ b/benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx @@ -0,0 +1,221 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['mobile', 'desktop'], + ...{}, + }), + curve: d3Curve(curve), + fill: (row) => `url(#shadcn-area-${row.series})`, + fillOpacity: 0.4, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: twoSeries.map((series, index) => ({ + id: `shadcn-area-${series}`, + x1: 0, + y1: 1, + x2: 0, + y2: 0, + stops: [ + { offset: 0.05, color: shadcnColors[index], opacity: 0.08 }, + { offset: 0.95, color: shadcnColors[index], opacity: 0.8 }, + ], + })), + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Gradient

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/135-shadcn-area-gradient/styles.css b/benchmarks/conformance/cases/135-shadcn-area-gradient/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/135-shadcn-area-gradient/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/135-shadcn-area-gradient/tanstack.ts b/benchmarks/conformance/cases/135-shadcn-area-gradient/tanstack.ts index ef61cdf2..c38602c7 100644 --- a/benchmarks/conformance/cases/135-shadcn-area-gradient/tanstack.ts +++ b/benchmarks/conformance/cases/135-shadcn-area-gradient/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-gradient') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Gradient implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx b/benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx new file mode 100644 index 00000000..042aa6b6 --- /dev/null +++ b/benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx @@ -0,0 +1,230 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['mobile', 'desktop'], + ...{}, + }), + curve: d3Curve(curve), + fill: undefined, + fillOpacity: 0.4, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 - 27 + const headerAction = null + const legend = + const footer = + return ( +
+
+
+
+

Area Chart - Icons

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + + + {titleCase(label)} + + ))} + + ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/136-shadcn-area-icons/styles.css b/benchmarks/conformance/cases/136-shadcn-area-icons/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/136-shadcn-area-icons/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/136-shadcn-area-icons/tanstack.ts b/benchmarks/conformance/cases/136-shadcn-area-icons/tanstack.ts index 9abb14da..a1d91fc5 100644 --- a/benchmarks/conformance/cases/136-shadcn-area-icons/tanstack.ts +++ b/benchmarks/conformance/cases/136-shadcn-area-icons/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-icons') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Icons implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx b/benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx new file mode 100644 index 00000000..f9a24907 --- /dev/null +++ b/benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx @@ -0,0 +1,327 @@ +import { useMemo, useState } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import interactiveAreaData from '@charts-poc/demo-data/shadcn-area-interactive-data' +import './styles.css' +type InteractiveTimeRange = '90d' | '30d' | '7d' +const twoSeries = ['desktop', 'mobile'] as const +const interactiveAreaRows = interactiveAreaData as readonly { + date: string + desktop: number + mobile: number +}[] +function createDefinition(timeRange: InteractiveTimeRange = '90d') { + const filteredRows = filterInteractiveAreaRows(timeRange) + const rows: ShadcnSeriesDatum[] = filteredRows.flatMap((row) => [ + { month: row.date, series: 'mobile', value: row.mobile }, + { month: row.date, series: 'desktop', value: row.desktop }, + ]) + return defineChart(({ width }) => ({ + marks: [ + areaY(rows, { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ order: ['mobile', 'desktop'] }), + curve: d3Curve(curveNatural), + fill: (row) => `url(#shadcn-interactive-${row.series})`, + fillOpacity: 1, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1, + }), + ], + x: { + scale: scalePoint, + axis: { + line: false, + ticks: { + values: interactiveDateTicks(timeRange), + size: 0, + padding: 10, + format: formatMonthDay, + }, + }, + }, + y: { + scale: scaleLinear().domain([0, 1200]), + grid: true, + axis: { + line: false, + ticks: { values: [0, 300, 600, 900, 1200], size: 0 }, + tickLabels: false, + }, + }, + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + gradients: twoSeries.map((series, index) => ({ + id: `shadcn-interactive-${series}`, + x1: 0, + y1: 1, + x2: 0, + y2: 0, + stops: [ + { offset: 0.05, color: shadcnColors[index], opacity: 0.1 }, + { offset: 0.95, color: shadcnColors[index], opacity: 0.8 }, + ], + })), + margin: { + top: 32, + right: width < 400 ? 14 : 5, + bottom: 35, + left: 5, + }, + theme: shadcnTheme(), + })) +} +function filterInteractiveAreaRows(timeRange: InteractiveTimeRange) { + const days = timeRange === '30d' ? 30 : timeRange === '7d' ? 7 : 90 + const start = new Date('2024-06-30T00:00:00Z') + start.setUTCDate(start.getUTCDate() - days) + const firstDate = start.toISOString().slice(0, 10) + return interactiveAreaRows.filter((row) => row.date >= firstDate) +} +function interactiveDateTicks(timeRange: InteractiveTimeRange) { + if (timeRange === '7d') { + return ['2024-06-23', '2024-06-25', '2024-06-27', '2024-06-29'] + } + if (timeRange === '30d') { + return [ + '2024-06-01', + '2024-06-08', + '2024-06-15', + '2024-06-22', + '2024-06-29', + ] + } + return [ + '2024-04-10', + '2024-04-21', + '2024-05-02', + '2024-05-13', + '2024-05-25', + '2024-06-05', + '2024-06-16', + '2024-06-29', + ] +} +function formatMonthDay(value: string) { + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format(new Date(value)) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart(timeRange: InteractiveTimeRange = '90d') { + return defineChart(createDefinition(timeRange), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const [timeRange, setTimeRange] = useState('90d') + const chartDefinition = useMemo( + () => createExampleChart(timeRange), + [timeRange], + ) + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = 250 + const headerAction = ( + setTimeRange(value as InteractiveTimeRange)} + options={[ + { value: '90d', label: 'Last 3 months' }, + { value: '30d', label: 'Last 30 days' }, + { value: '7d', label: 'Last 7 days' }, + ]} + /> + ) + const legend = + const footer = null + return ( +
+
+
+
+

Area Chart - Interactive

+

Showing total visitors for the last 3 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + {titleCase(label)} + + ))} + + ) +} +function SelectControl({ + value, + options, + onChange, +}: { + value: string + options: readonly { + value: string + label: string + swatch?: string + }[] + onChange: (value: string) => void +}) { + const selected = options.find((option) => option.value === value) + return ( + + ) +} diff --git a/benchmarks/conformance/cases/137-shadcn-area-interactive/styles.css b/benchmarks/conformance/cases/137-shadcn-area-interactive/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/137-shadcn-area-interactive/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/137-shadcn-area-interactive/tanstack.ts b/benchmarks/conformance/cases/137-shadcn-area-interactive/tanstack.ts index 818e8025..7dda24f1 100644 --- a/benchmarks/conformance/cases/137-shadcn-area-interactive/tanstack.ts +++ b/benchmarks/conformance/cases/137-shadcn-area-interactive/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-interactive') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Interactive implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx b/benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx new file mode 100644 index 00000000..b472b488 --- /dev/null +++ b/benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx @@ -0,0 +1,225 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['mobile', 'desktop'], + ...{}, + }), + curve: d3Curve(curve), + fill: undefined, + fillOpacity: 0.4, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 - 27 + const headerAction = null + const legend = + const footer = + return ( +
+
+
+
+

Area Chart - Legend

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + {titleCase(label)} + + ))} + + ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/138-shadcn-area-legend/styles.css b/benchmarks/conformance/cases/138-shadcn-area-legend/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/138-shadcn-area-legend/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/138-shadcn-area-legend/tanstack.ts b/benchmarks/conformance/cases/138-shadcn-area-legend/tanstack.ts index eb2d937c..7638145b 100644 --- a/benchmarks/conformance/cases/138-shadcn-area-legend/tanstack.ts +++ b/benchmarks/conformance/cases/138-shadcn-area-legend/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-legend') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Legend implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx b/benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx new file mode 100644 index 00000000..26b4dfcd --- /dev/null +++ b/benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinear } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveLinear + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-area', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + fill: shadcnColors[0], + fillOpacity: 0.4, + stroke: shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Linear

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/139-shadcn-area-linear/styles.css b/benchmarks/conformance/cases/139-shadcn-area-linear/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/139-shadcn-area-linear/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/139-shadcn-area-linear/tanstack.ts b/benchmarks/conformance/cases/139-shadcn-area-linear/tanstack.ts index d236175c..aa513338 100644 --- a/benchmarks/conformance/cases/139-shadcn-area-linear/tanstack.ts +++ b/benchmarks/conformance/cases/139-shadcn-area-linear/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-linear') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Linear implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/14-error-bars/example.tsx b/benchmarks/conformance/cases/14-error-bars/example.tsx new file mode 100644 index 00000000..f5b15d97 --- /dev/null +++ b/benchmarks/conformance/cases/14-error-bars/example.tsx @@ -0,0 +1,110 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' +import { + defineChart, + deviation, + dot, + groupBy, + link, + tickY, +} from '@tanstack/charts' +import type { TransformReduceContext } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +const estimate = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 1, + maximumFractionDigits: 1, +}) + +export const sampleDeviation = (context: TransformReduceContext) => + context.values.length < 2 ? 0 : deviation(context) + +export const errorBarsDefinition = (input: ExampleOptions) => { + const observations = penguins + .slice(input.revision * 8) + .filter( + (row): row is PenguinsRow & { body_mass_g: number } => + row.body_mass_g !== null, + ) + const rows = groupBy(observations, { + by: 'species', + outputs: { + mean: { value: 'body_mass_g', reduce: 'mean' }, + deviation: { value: 'body_mass_g', reduce: sampleDeviation }, + }, + }) + + return defineChart({ + marks: [ + link(rows, { + id: 'error-interval', + x1: 'species', + y1: ({ mean, deviation: spread }) => mean - spread, + x2: 'species', + y2: ({ mean, deviation: spread }) => mean + spread, + stroke: '#2563eb', + strokeWidth: 1.5, + }), + tickY(rows, { + id: 'error-low', + x: 'species', + y: ({ mean, deviation: spread }) => mean - spread, + stroke: '#2563eb', + strokeWidth: 1.5, + }), + tickY(rows, { + id: 'error-high', + x: 'species', + y: ({ mean, deviation: spread }) => mean + spread, + stroke: '#2563eb', + strokeWidth: 1.5, + }), + dot(rows, { + id: 'error-mean', + x: 'species', + y: 'mean', + key: 'species', + fill: '#2563eb', + r: 3.5, + }), + ], + x: { + scale: () => scaleBand().padding(0.22), + }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Point estimates with error bars' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(errorBarsDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `${point.datum.species} · Mean: ${estimate.format(point.datum.mean)} g · Range: ${estimate.format(point.datum.mean - point.datum.deviation)}–${estimate.format(point.datum.mean + point.datum.deviation)} g`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/14-error-bars/tanstack.test.ts b/benchmarks/conformance/cases/14-error-bars/tanstack.test.ts index c0254596..3ca74dab 100644 --- a/benchmarks/conformance/cases/14-error-bars/tanstack.test.ts +++ b/benchmarks/conformance/cases/14-error-bars/tanstack.test.ts @@ -123,7 +123,7 @@ describe('definition-owned error bars', () => { 'benchmarks/conformance/cases/14-error-bars', ) const tanstackSource = readFileSync( - resolve(caseDirectory, 'tanstack.ts'), + resolve(caseDirectory, 'example.tsx'), 'utf8', ) const plotSource = readFileSync(resolve(caseDirectory, 'plot.ts'), 'utf8') diff --git a/benchmarks/conformance/cases/14-error-bars/tanstack.ts b/benchmarks/conformance/cases/14-error-bars/tanstack.ts index 46d0df16..6c029e65 100644 --- a/benchmarks/conformance/cases/14-error-bars/tanstack.ts +++ b/benchmarks/conformance/cases/14-error-bars/tanstack.ts @@ -1,89 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import { - defineChart, - deviation, - dot, - groupBy, - link, - tickY, -} from '@tanstack/charts' -import type { TransformReduceContext } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const estimate = new Intl.NumberFormat('en-US', { - minimumFractionDigits: 1, - maximumFractionDigits: 1, -}) +export * from './example' -export const sampleDeviation = (context: TransformReduceContext) => - context.values.length < 2 ? 0 : deviation(context) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const errorBarsDefinition = (input: ConformanceInput) => { - const observations = penguins - .slice(input.revision * 8) - .filter( - (row): row is PenguinsRow & { body_mass_g: number } => - row.body_mass_g !== null, - ) - const rows = groupBy(observations, { - by: 'species', - outputs: { - mean: { value: 'body_mass_g', reduce: 'mean' }, - deviation: { value: 'body_mass_g', reduce: sampleDeviation }, - }, - }) - - return defineChart({ - marks: [ - link(rows, { - id: 'error-interval', - x1: 'species', - y1: ({ mean, deviation: spread }) => mean - spread, - x2: 'species', - y2: ({ mean, deviation: spread }) => mean + spread, - stroke: '#2563eb', - strokeWidth: 1.5, - }), - tickY(rows, { - id: 'error-low', - x: 'species', - y: ({ mean, deviation: spread }) => mean - spread, - stroke: '#2563eb', - strokeWidth: 1.5, - }), - tickY(rows, { - id: 'error-high', - x: 'species', - y: ({ mean, deviation: spread }) => mean + spread, - stroke: '#2563eb', - strokeWidth: 1.5, - }), - dot(rows, { - id: 'error-mean', - x: 'species', - y: 'mean', - key: 'species', - fill: '#2563eb', - r: 3.5, - }), - ], - x: { - scale: () => scaleBand().padding(0.22), - }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, - }) -} - -export const catalogCase = tanstackCase( - errorBarsDefinition, - 'Point estimates with error bars', - { - format: (point) => - `${point.datum.species} · Mean: ${estimate.format(point.datum.mean)} g · Range: ${estimate.format(point.datum.mean - point.datum.deviation)}–${estimate.format(point.datum.mean + point.datum.deviation)} g`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx new file mode 100644 index 00000000..78173b29 --- /dev/null +++ b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx @@ -0,0 +1,225 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + const expandedRows = shadcnMonths.flatMap((row, index) => [ + { + month: row.month, + series: 'other', + value: [45, 100, 150, 50, 100, 160][index]!, + }, + { month: row.month, series: 'mobile', value: row.mobile }, + { month: row.month, series: 'desktop', value: row.desktop }, + ]) + const rows = expandedRows + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['other', 'mobile', 'desktop'], + ...{ offset: 'normalize' }, + }), + curve: d3Curve(curve), + fill: (row) => + row.series === 'other' + ? `color-mix(in oklch, ${shadcnColors[2]} 10%, transparent)` + : `color-mix(in oklch, ${row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0]} 40%, transparent)`, + fillOpacity: 1, + stroke: (row) => + row.series === 'other' + ? shadcnColors[2] + : row.series === 'mobile' + ? shadcnColors[1] + : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 1]), + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: ['other', 'mobile', 'desktop'], + range: [shadcnColors[2], shadcnColors[1], shadcnColors[0]], + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Stacked Expanded

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/styles.css b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/tanstack.ts b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/tanstack.ts index d5fbb277..42d6b419 100644 --- a/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/tanstack.ts +++ b/benchmarks/conformance/cases/140-shadcn-area-stacked-expand/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-stacked-expand') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Stacked Expanded implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx b/benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx new file mode 100644 index 00000000..c4790fef --- /dev/null +++ b/benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx @@ -0,0 +1,210 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveNatural + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-areas', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ + order: ['mobile', 'desktop'], + ...{}, + }), + curve: d3Curve(curve), + fill: undefined, + fillOpacity: 0.4, + stroke: (row) => + row.series === 'mobile' ? shadcnColors[1] : shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Stacked

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/141-shadcn-area-stacked/styles.css b/benchmarks/conformance/cases/141-shadcn-area-stacked/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/141-shadcn-area-stacked/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/141-shadcn-area-stacked/tanstack.ts b/benchmarks/conformance/cases/141-shadcn-area-stacked/tanstack.ts index 3dcf9595..dfd044bc 100644 --- a/benchmarks/conformance/cases/141-shadcn-area-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/141-shadcn-area-stacked/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-stacked') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Stacked implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/142-shadcn-area-step/example.tsx b/benchmarks/conformance/cases/142-shadcn-area-step/example.tsx new file mode 100644 index 00000000..31026c0d --- /dev/null +++ b/benchmarks/conformance/cases/142-shadcn-area-step/example.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react' +import { + areaY, + d3Curve, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveStep } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const monthSeries = ['desktop', 'mobile', 'tablet'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveStep + return defineChart({ + marks: [ + areaY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-area', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + fill: shadcnColors[0], + fillOpacity: 0.4, + stroke: shadcnColors[0], + strokeWidth: 1.5, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear, + grid: true, + axis: { line: false, ticks: false, tickLabels: false }, + }, + color: { + domain: monthSeries, + range: shadcnColors.slice(0, 3), + }, + gradients: undefined, + margin: { top: 5, right: 12, bottom: 35, left: 12 }, + theme: shadcnTheme(), + }) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Area Chart - Step

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/142-shadcn-area-step/styles.css b/benchmarks/conformance/cases/142-shadcn-area-step/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/142-shadcn-area-step/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/142-shadcn-area-step/tanstack.ts b/benchmarks/conformance/cases/142-shadcn-area-step/tanstack.ts index 8c9f95bc..eb2edc0f 100644 --- a/benchmarks/conformance/cases/142-shadcn-area-step/tanstack.ts +++ b/benchmarks/conformance/cases/142-shadcn-area-step/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-area-step') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Area Chart - Step implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx b/benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx new file mode 100644 index 00000000..f18f5a96 --- /dev/null +++ b/benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx @@ -0,0 +1,198 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const rows = shadcnBrowsers.map((row) => + row.browser === 'chrome' + ? { ...row, visitors: 187 } + : row.browser === 'firefox' + ? { ...row, visitors: 275 } + : row, + ) + return defineChart({ + marks: [ + barY(rows, { + id: 'visitor-bars', + x: 'browser', + y: 'visitors', + color: 'browser', + fill: (row) => + row.browser === 'firefox' + ? `color-mix(in srgb, ${shadcnColors[2]} 80%, transparent)` + : shadcnColors[browserNames.indexOf(row.browser)]!, + stroke: (row) => (row.browser === 'firefox' ? shadcnColors[2] : 'none'), + strokeWidth: 2, + strokeDasharray: (row) => (row.browser === 'firefox' ? '4 4' : 'none'), + radius: 8, + }), + ], + x: shadcnBrowserXAxis(), + y: { scale: scaleLinear, grid: true, axis: false }, + color: { domain: browserNames, range: shadcnColors }, + margin: { top: 5, right: 5, bottom: 35, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnBrowserXAxis() { + return { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: titleCase, + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Active

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/143-shadcn-bar-active/styles.css b/benchmarks/conformance/cases/143-shadcn-bar-active/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/143-shadcn-bar-active/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/143-shadcn-bar-active/tanstack.ts b/benchmarks/conformance/cases/143-shadcn-bar-active/tanstack.ts index eec46eee..d7a1b080 100644 --- a/benchmarks/conformance/cases/143-shadcn-bar-active/tanstack.ts +++ b/benchmarks/conformance/cases/143-shadcn-bar-active/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-active') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Active implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx b/benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx new file mode 100644 index 00000000..8cff7136 --- /dev/null +++ b/benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx @@ -0,0 +1,185 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnMonths } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + const rows = shadcnMonths + const marks = [ + barY(rows, { + id: 'visitor-bars', + x: 'month', + y: 'desktop', + fill: (row) => + false && row.desktop < 0 ? shadcnColors[1] : shadcnColors[0], + radius: 8, + }), + ] + return defineChart({ + marks, + x: shadcnXAxis(), + y: { scale: scaleLinear, grid: true, axis: false }, + margin: { top: 5, right: 5, bottom: 35, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnXAxis() { + return { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/144-shadcn-bar-default/styles.css b/benchmarks/conformance/cases/144-shadcn-bar-default/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/144-shadcn-bar-default/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/144-shadcn-bar-default/tanstack.ts b/benchmarks/conformance/cases/144-shadcn-bar-default/tanstack.ts index 10bef265..141b0c28 100644 --- a/benchmarks/conformance/cases/144-shadcn-bar-default/tanstack.ts +++ b/benchmarks/conformance/cases/144-shadcn-bar-default/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-default') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx new file mode 100644 index 00000000..877e3946 --- /dev/null +++ b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx @@ -0,0 +1,174 @@ +import { useMemo } from 'react' +import { + barX, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnMonths } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + const rows = shadcnMonths + const marks = [ + barX(rows, { + id: 'visitor-bars', + x: 'desktop', + y: 'month', + fill: shadcnColors[0], + radius: 5, + }), + ] + return defineChart({ + marks, + x: { scale: scaleLinear, axis: false }, + y: { + scale: () => scaleBand().paddingInner(0.18).paddingOuter(0.08), + axis: { line: false, ticks: { size: 0 } }, + }, + margin: { top: 5, right: 5, bottom: 5, left: 72 }, + theme: shadcnTheme(), + }) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Horizontal

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/145-shadcn-bar-horizontal/styles.css b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/145-shadcn-bar-horizontal/tanstack.ts b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/tanstack.ts index ee3573ba..10e3b1f5 100644 --- a/benchmarks/conformance/cases/145-shadcn-bar-horizontal/tanstack.ts +++ b/benchmarks/conformance/cases/145-shadcn-bar-horizontal/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-horizontal') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Horizontal implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx b/benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx new file mode 100644 index 00000000..515da8d7 --- /dev/null +++ b/benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx @@ -0,0 +1,239 @@ +import { useMemo, useState } from 'react' +import { + barY, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnColors, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import interactiveAreaData from '@charts-poc/demo-data/shadcn-area-interactive-data' +import './styles.css' +type InteractiveSeries = 'desktop' | 'mobile' +const twoSeries = ['desktop', 'mobile'] as const +const interactiveAreaRows = interactiveAreaData as readonly { + date: string + desktop: number + mobile: number +}[] +const interactiveBarRows: readonly ShadcnMonthDatum[] = interactiveAreaRows.map( + (row) => ({ + month: row.date, + desktop: row.desktop, + mobile: row.mobile, + tablet: 0, + }), +) +function createDefinition(activeSeries: InteractiveSeries = 'desktop') { + return defineChart({ + marks: [ + barY(interactiveBarRows, { + id: 'daily-bars', + x: 'month', + y: (row) => row[activeSeries], + z: () => activeSeries, + key: 'month', + fill: activeSeries === 'desktop' ? shadcnColors[1] : shadcnColors[0], + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + values: [ + '2024-04-01', + '2024-04-11', + '2024-04-22', + '2024-05-03', + '2024-05-14', + '2024-05-26', + '2024-06-06', + '2024-06-17', + '2024-06-29', + ], + size: 0, + padding: 10, + format: formatMonthDay, + }, + }, + }, + y: { scale: scaleLinear, grid: true, axis: false }, + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: { top: 5, right: 12, bottom: 25, left: 12 }, + theme: shadcnTheme(), + }) +} +function formatMonthDay(value: string) { + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format(new Date(value)) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart( + activeSeries: InteractiveSeries = 'desktop', +) { + return defineChart(createDefinition(activeSeries), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const [activeSeries, setActiveSeries] = useState('desktop') + const chartDefinition = useMemo( + () => createExampleChart(activeSeries), + [activeSeries], + ) + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = 250 + const headerAction = ( + + ) + const legend = null + const footer = null + return ( +
+
+
+
+

Bar Chart - Interactive

+

Showing total visitors for the last 3 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function BarMetrics({ + active, + onChange, +}: { + active: InteractiveSeries + onChange: (series: InteractiveSeries) => void +}) { + const totals = { + desktop: interactiveBarRows.reduce((sum, row) => sum + row.desktop, 0), + mobile: interactiveBarRows.reduce((sum, row) => sum + row.mobile, 0), + } + return ( + <> + {twoSeries.map((series) => ( + + ))} + + ) +} diff --git a/benchmarks/conformance/cases/146-shadcn-bar-interactive/styles.css b/benchmarks/conformance/cases/146-shadcn-bar-interactive/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/146-shadcn-bar-interactive/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/146-shadcn-bar-interactive/tanstack.ts b/benchmarks/conformance/cases/146-shadcn-bar-interactive/tanstack.ts index 10495e83..5ac0262a 100644 --- a/benchmarks/conformance/cases/146-shadcn-bar-interactive/tanstack.ts +++ b/benchmarks/conformance/cases/146-shadcn-bar-interactive/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-interactive') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Interactive implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx new file mode 100644 index 00000000..bb98a5c6 --- /dev/null +++ b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx @@ -0,0 +1,193 @@ +import { useMemo } from 'react' +import { + barX, + defineChart, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnMonths } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + return defineChart({ + marks: [ + barX(shadcnMonths, { + id: 'visitor-bars', + x: 'desktop', + y: 'month', + fill: shadcnColors[1], + radius: 4, + }), + text(shadcnMonths, { + id: 'month-labels', + x: () => 0, + y: 'month', + text: 'month', + dx: 8, + anchor: 'start', + fill: 'var(--background)', + fontSize: 12, + }), + text(shadcnMonths, { + id: 'visitor-labels', + x: 'desktop', + y: 'month', + text: (row) => row.desktop.toLocaleString('en-US'), + dx: 8, + anchor: 'start', + fill: 'var(--foreground)', + fontSize: 12, + }), + ], + x: { scale: scaleLinear, grid: true, axis: false }, + y: { + scale: () => scaleBand().paddingInner(0.18).paddingOuter(0.08), + axis: false, + }, + margin: { top: 5, right: 36, bottom: 5, left: 0 }, + theme: shadcnTheme(), + }) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Custom Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/147-shadcn-bar-label-custom/styles.css b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/147-shadcn-bar-label-custom/tanstack.ts b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/tanstack.ts index ddb3cd9d..1dc4cf3b 100644 --- a/benchmarks/conformance/cases/147-shadcn-bar-label-custom/tanstack.ts +++ b/benchmarks/conformance/cases/147-shadcn-bar-label-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-label-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Custom Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx b/benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx new file mode 100644 index 00000000..41ed111a --- /dev/null +++ b/benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx @@ -0,0 +1,197 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnMonths } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + const rows = shadcnMonths + const marks = [ + barY(rows, { + id: 'visitor-bars', + x: 'month', + y: 'desktop', + fill: (row) => + false && row.desktop < 0 ? shadcnColors[1] : shadcnColors[0], + radius: 8, + }), + text(rows, { + id: 'visitor-labels', + x: 'month', + y: 'desktop', + text: (row) => row.desktop.toLocaleString('en-US'), + ...{ + dy: -12, + fill: 'var(--foreground)', + fontSize: 12, + }, + }), + ] + return defineChart({ + marks, + x: shadcnXAxis(), + y: { scale: scaleLinear, grid: true, axis: false }, + margin: { top: 24, right: 5, bottom: 35, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnXAxis() { + return { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/148-shadcn-bar-label/styles.css b/benchmarks/conformance/cases/148-shadcn-bar-label/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/148-shadcn-bar-label/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/148-shadcn-bar-label/tanstack.ts b/benchmarks/conformance/cases/148-shadcn-bar-label/tanstack.ts index 24b06bb8..84069216 100644 --- a/benchmarks/conformance/cases/148-shadcn-bar-label/tanstack.ts +++ b/benchmarks/conformance/cases/148-shadcn-bar-label/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-label') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx b/benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx new file mode 100644 index 00000000..40b74985 --- /dev/null +++ b/benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx @@ -0,0 +1,180 @@ +import { useMemo } from 'react' +import { + barX, + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + return defineChart({ + marks: [ + barX(shadcnBrowsers, { + id: 'visitor-bars', + x: 'visitors', + y: 'browser', + color: 'browser', + radius: 5, + }), + ], + x: { scale: scaleLinear, axis: false }, + y: shadcnBrowserYAxis(), + color: { domain: browserNames, range: shadcnColors }, + margin: { top: 5, right: 5, bottom: 5, left: 60 }, + theme: shadcnTheme(), + }) +} +function shadcnBrowserYAxis() { + return { + scale: () => scaleBand().paddingInner(0.18).paddingOuter(0.08), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: titleCase }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Mixed

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/149-shadcn-bar-mixed/styles.css b/benchmarks/conformance/cases/149-shadcn-bar-mixed/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/149-shadcn-bar-mixed/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/149-shadcn-bar-mixed/tanstack.ts b/benchmarks/conformance/cases/149-shadcn-bar-mixed/tanstack.ts index 2613aba7..51135e26 100644 --- a/benchmarks/conformance/cases/149-shadcn-bar-mixed/tanstack.ts +++ b/benchmarks/conformance/cases/149-shadcn-bar-mixed/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-mixed') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Mixed implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/15-boxplot/example.tsx b/benchmarks/conformance/cases/15-boxplot/example.tsx new file mode 100644 index 00000000..55917a9e --- /dev/null +++ b/benchmarks/conformance/cases/15-boxplot/example.tsx @@ -0,0 +1,67 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { boxY, defineChart } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import { morley } from '@charts-poc/demo-data/morley' + +export const boxplotDefinition = () => + defineChart({ + marks: [ + boxY(morley, { + id: 'morley-boxplot', + x: 'Expt', + y: 'Speed', + key: (row) => `${row.Expt}:${row.Run}`, + fill: '#bfdbfe', + stroke: '#2563eb', + inset: 18, + r: 2.5, + }), + ], + x: { + scale: () => scaleBand().padding(0.22), + axis: { label: 'Experiment' }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Speed of light (km/s minus 299,000)' }, + }, + }) + +export const exampleAriaLabel = 'Grouped boxplots' + +export const createExampleChart = () => + defineChart(boxplotDefinition(), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + datum.kind === 'summary' + ? `Experiment ${datum.category} · median ${datum.median.toLocaleString( + 'en-US', + { + maximumFractionDigits: 1, + }, + )} · IQR ${datum.q1.toLocaleString('en-US', { + maximumFractionDigits: 1, + })}–${datum.q3.toLocaleString('en-US', { + maximumFractionDigits: 1, + })}` + : `Experiment ${datum.category} outlier · ${datum.value.toLocaleString( + 'en-US', + { + maximumFractionDigits: 1, + }, + )}`, + }, + }, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/15-boxplot/tanstack.test.ts b/benchmarks/conformance/cases/15-boxplot/tanstack.test.ts index 5dc964bb..bb6ae7bd 100644 --- a/benchmarks/conformance/cases/15-boxplot/tanstack.test.ts +++ b/benchmarks/conformance/cases/15-boxplot/tanstack.test.ts @@ -57,7 +57,7 @@ describe('native boxplot definition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/15-boxplot/tanstack.ts', + 'benchmarks/conformance/cases/15-boxplot/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/15-boxplot/tanstack.ts b/benchmarks/conformance/cases/15-boxplot/tanstack.ts index 72164c96..352e831c 100644 --- a/benchmarks/conformance/cases/15-boxplot/tanstack.ts +++ b/benchmarks/conformance/cases/15-boxplot/tanstack.ts @@ -1,52 +1,11 @@ -import { boxY, defineChart } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { morley } from '@charts-poc/demo-data/morley' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const boxplotDefinition = () => - defineChart({ - marks: [ - boxY(morley, { - id: 'morley-boxplot', - x: 'Expt', - y: 'Speed', - key: (row) => `${row.Expt}:${row.Run}`, - fill: '#bfdbfe', - stroke: '#2563eb', - inset: 18, - r: 2.5, - }), - ], - x: { - scale: () => scaleBand().padding(0.22), - axis: { label: 'Experiment' }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Speed of light (km/s minus 299,000)' }, - }, - }) +export * from './example' -export const catalogCase = tanstackCase(boxplotDefinition, 'Grouped boxplots', { - format: ({ datum }) => - datum.kind === 'summary' - ? `Experiment ${datum.category} · median ${datum.median.toLocaleString( - 'en-US', - { - maximumFractionDigits: 1, - }, - )} · IQR ${datum.q1.toLocaleString('en-US', { - maximumFractionDigits: 1, - })}–${datum.q3.toLocaleString('en-US', { - maximumFractionDigits: 1, - })}` - : `Experiment ${datum.category} outlier · ${datum.value.toLocaleString( - 'en-US', - { - maximumFractionDigits: 1, - }, - )}`, -}) +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, +) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx b/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx new file mode 100644 index 00000000..a0005f94 --- /dev/null +++ b/benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx @@ -0,0 +1,198 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnColors, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + const negativeRows = [ + { month: 'January', desktop: 186, mobile: 0, tablet: 0 }, + { month: 'February', desktop: 205, mobile: 0, tablet: 0 }, + { month: 'March', desktop: -207, mobile: 0, tablet: 0 }, + { month: 'April', desktop: 173, mobile: 0, tablet: 0 }, + { month: 'May', desktop: -209, mobile: 0, tablet: 0 }, + { month: 'June', desktop: 214, mobile: 0, tablet: 0 }, + ] + const rows = negativeRows + const marks = [ + barY(rows, { + id: 'visitor-bars', + x: 'month', + y: 'desktop', + fill: (row) => + true && row.desktop < 0 ? shadcnColors[1] : shadcnColors[0], + radius: 0, + }), + text(rows, { + id: 'visitor-labels', + x: 'month', + y: 'desktop', + ...{ + text: (row: ShadcnMonthDatum) => row.month, + dy: (row: ShadcnMonthDatum) => (row.desktop < 0 ? 14 : -10), + fill: shadcnColors[0], + fontSize: 12, + }, + }), + ] + return defineChart({ + marks, + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: false, + }, + y: { scale: scaleLinear, grid: true, axis: false }, + margin: { top: 24, right: 5, bottom: 24, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Negative

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/150-shadcn-bar-negative/styles.css b/benchmarks/conformance/cases/150-shadcn-bar-negative/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/150-shadcn-bar-negative/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/150-shadcn-bar-negative/tanstack.ts b/benchmarks/conformance/cases/150-shadcn-bar-negative/tanstack.ts index 1e5fa74b..752d0e1a 100644 --- a/benchmarks/conformance/cases/150-shadcn-bar-negative/tanstack.ts +++ b/benchmarks/conformance/cases/150-shadcn-bar-negative/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-negative') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Negative implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx b/benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx new file mode 100644 index 00000000..9dc63994 --- /dev/null +++ b/benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx @@ -0,0 +1,204 @@ +import { useMemo } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors, shadcnSeriesRows } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + return defineChart({ + marks: [ + barY(rows, { + id: 'visitor-bars', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + layout: stack({ order: twoSeries }), + radius: 4, + }), + ], + x: shadcnXAxis(), + y: { scale: scaleLinear, grid: true, axis: false }, + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: { top: 5, right: 5, bottom: 35, left: 5 }, + theme: shadcnTheme(), + }) +} +function shadcnXAxis() { + return { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 - 27 + const headerAction = null + const legend = + const footer = ( + + ) + return ( +
+
+
+
+

Bar Chart - Stacked + Legend

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + {titleCase(label)} + + ))} + + ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/151-shadcn-bar-stacked/styles.css b/benchmarks/conformance/cases/151-shadcn-bar-stacked/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/151-shadcn-bar-stacked/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/151-shadcn-bar-stacked/tanstack.ts b/benchmarks/conformance/cases/151-shadcn-bar-stacked/tanstack.ts index 5e899b85..67ebd8de 100644 --- a/benchmarks/conformance/cases/151-shadcn-bar-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/151-shadcn-bar-stacked/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-bar-stacked') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Bar Chart - Stacked + Legend implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/152-shadcn-line-default/example.tsx b/benchmarks/conformance/cases/152-shadcn-line-default/example.tsx new file mode 100644 index 00000000..7e110793 --- /dev/null +++ b/benchmarks/conformance/cases/152-shadcn-line-default/example.tsx @@ -0,0 +1,212 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveNatural + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/152-shadcn-line-default/styles.css b/benchmarks/conformance/cases/152-shadcn-line-default/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/152-shadcn-line-default/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/152-shadcn-line-default/tanstack.ts b/benchmarks/conformance/cases/152-shadcn-line-default/tanstack.ts index 96824eac..83d5fc7b 100644 --- a/benchmarks/conformance/cases/152-shadcn-line-default/tanstack.ts +++ b/benchmarks/conformance/cases/152-shadcn-line-default/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-default') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx new file mode 100644 index 00000000..2ae15a38 --- /dev/null +++ b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx @@ -0,0 +1,205 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + dot, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnBrowsers, + shadcnColors, + type ShadcnBrowserDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const rows = shadcnBrowsers + const curve = curveNatural + const dotMarks = [ + dot(shadcnBrowsers, { + id: 'visitor-dots', + x: 'browser', + y: 'visitors', + color: 'browser', + r: 5, + }), + ] + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnBrowserDatum[], { + id: 'visitor-line', + x: 'browser', + y: 'visitors', + curve: d3Curve(curve), + stroke: shadcnColors[1], + strokeWidth: 2, + }), + ...dotMarks, + ], + x: { scale: scalePoint, axis: false }, + y: { + scale: scaleLinear().domain([0, 300]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 75, 150, 225, 300], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: browserNames, + range: shadcnColors, + }, + margin: { top: 24, right: 24, bottom: 5, left: 24 }, + theme: shadcnTheme(), + })) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Dots Colors

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/153-shadcn-line-dots-colors/styles.css b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/153-shadcn-line-dots-colors/tanstack.ts b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/tanstack.ts index 9dc257d9..d0838e4d 100644 --- a/benchmarks/conformance/cases/153-shadcn-line-dots-colors/tanstack.ts +++ b/benchmarks/conformance/cases/153-shadcn-line-dots-colors/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-dots-colors') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Dots Colors implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx new file mode 100644 index 00000000..0592a635 --- /dev/null +++ b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx @@ -0,0 +1,234 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + dot, + lineY, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveNatural + const dotMarks = [ + text(shadcnMonths, { + id: 'visitor-dot-stems', + x: 'month', + y: 'desktop', + text: () => '│', + fill: shadcnColors[0], + fontSize: 24, + }), + dot(shadcnMonths, { + id: 'visitor-dots', + x: 'month', + y: 'desktop', + r: 5, + fill: 'var(--background)', + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ] + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ...dotMarks, + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: true && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Custom Dots

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/154-shadcn-line-dots-custom/styles.css b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/154-shadcn-line-dots-custom/tanstack.ts b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/tanstack.ts index 2540373e..6c009c17 100644 --- a/benchmarks/conformance/cases/154-shadcn-line-dots-custom/tanstack.ts +++ b/benchmarks/conformance/cases/154-shadcn-line-dots-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-dots-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Custom Dots implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx b/benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx new file mode 100644 index 00000000..3127a7dd --- /dev/null +++ b/benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx @@ -0,0 +1,225 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + dot, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveNatural + const dotMarks = [ + dot(shadcnMonths, { + id: 'visitor-dots', + x: 'month', + y: 'desktop', + r: 4, + fill: shadcnColors[0], + stroke: shadcnColors[0], + strokeWidth: 1, + }), + ] + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ...dotMarks, + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Dots

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/155-shadcn-line-dots/styles.css b/benchmarks/conformance/cases/155-shadcn-line-dots/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/155-shadcn-line-dots/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/155-shadcn-line-dots/tanstack.ts b/benchmarks/conformance/cases/155-shadcn-line-dots/tanstack.ts index bc15ac6b..64b93c57 100644 --- a/benchmarks/conformance/cases/155-shadcn-line-dots/tanstack.ts +++ b/benchmarks/conformance/cases/155-shadcn-line-dots/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-dots') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Dots implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx b/benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx new file mode 100644 index 00000000..eacaf275 --- /dev/null +++ b/benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx @@ -0,0 +1,255 @@ +import { useMemo, useState } from 'react' +import { + d3Curve, + defineChart, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveMonotoneX } from 'd3-shape' +import { + shadcnColors, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import interactiveAreaData from '@charts-poc/demo-data/shadcn-area-interactive-data' +import './styles.css' +type InteractiveSeries = 'desktop' | 'mobile' +const twoSeries = ['desktop', 'mobile'] as const +const interactiveAreaRows = interactiveAreaData as readonly { + date: string + desktop: number + mobile: number +}[] +const interactiveBarRows: readonly ShadcnMonthDatum[] = interactiveAreaRows.map( + (row) => ({ + month: row.date, + desktop: row.desktop, + mobile: row.mobile, + tablet: 0, + }), +) +function createDefinition(activeSeries: InteractiveSeries = 'desktop') { + return defineChart(({ width }) => ({ + marks: [ + lineY(interactiveBarRows, { + id: 'daily-line', + x: 'month', + y: (row) => row[activeSeries], + z: () => activeSeries, + key: 'month', + curve: d3Curve(curveMonotoneX), + stroke: activeSeries === 'desktop' ? shadcnColors[0] : shadcnColors[1], + strokeWidth: 2, + }), + ], + x: { + scale: scalePoint, + axis: { + line: false, + ticks: { + values: [ + '2024-04-10', + '2024-04-21', + '2024-05-02', + '2024-05-13', + '2024-05-25', + '2024-06-05', + '2024-06-16', + '2024-06-29', + ], + size: 0, + padding: 10, + format: formatMonthDay, + }, + }, + }, + y: { + scale: scaleLinear().domain([0, 600]), + grid: true, + axis: { + line: false, + ticks: { values: [0, 150, 300, 450, 600], size: 0 }, + tickLabels: false, + }, + }, + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: { + top: 5, + right: width < 400 ? 24 : 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function formatMonthDay(value: string) { + return new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format(new Date(value)) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart( + activeSeries: InteractiveSeries = 'desktop', +) { + return defineChart(createDefinition(activeSeries), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const [activeSeries, setActiveSeries] = useState('desktop') + const chartDefinition = useMemo( + () => createExampleChart(activeSeries), + [activeSeries], + ) + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = 250 + const headerAction = ( + + ) + const legend = null + const footer = null + return ( +
+
+
+
+

Line Chart - Interactive

+

Showing total visitors for the last 3 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function BarMetrics({ + active, + onChange, +}: { + active: InteractiveSeries + onChange: (series: InteractiveSeries) => void +}) { + const totals = { + desktop: interactiveBarRows.reduce((sum, row) => sum + row.desktop, 0), + mobile: interactiveBarRows.reduce((sum, row) => sum + row.mobile, 0), + } + return ( + <> + {twoSeries.map((series) => ( + + ))} + + ) +} diff --git a/benchmarks/conformance/cases/156-shadcn-line-interactive/styles.css b/benchmarks/conformance/cases/156-shadcn-line-interactive/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/156-shadcn-line-interactive/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/156-shadcn-line-interactive/tanstack.ts b/benchmarks/conformance/cases/156-shadcn-line-interactive/tanstack.ts index 16e6285b..1d0523e7 100644 --- a/benchmarks/conformance/cases/156-shadcn-line-interactive/tanstack.ts +++ b/benchmarks/conformance/cases/156-shadcn-line-interactive/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-interactive') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Interactive implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx b/benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx new file mode 100644 index 00000000..64d2601f --- /dev/null +++ b/benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx @@ -0,0 +1,207 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnBrowsers, + shadcnColors, + type ShadcnBrowserDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const rows = shadcnBrowsers + const curve = curveNatural + const labelMarks = [ + text(shadcnBrowsers, { + id: 'visitor-labels', + x: 'browser', + y: 'visitors', + text: (row) => titleCase(row.browser), + dy: -12, + fill: 'var(--foreground)', + fontSize: 12, + }), + ] + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnBrowserDatum[], { + id: 'visitor-line', + x: 'browser', + y: 'visitors', + curve: d3Curve(curve), + stroke: shadcnColors[1], + strokeWidth: 2, + }), + ...labelMarks, + ], + x: { scale: scalePoint, axis: false }, + y: { + scale: scaleLinear().domain([0, 300]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 75, 150, 225, 300], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: browserNames, + range: shadcnColors, + }, + margin: { top: 24, right: 24, bottom: 5, left: 24 }, + theme: shadcnTheme(), + })) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Custom Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/157-shadcn-line-label-custom/styles.css b/benchmarks/conformance/cases/157-shadcn-line-label-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/157-shadcn-line-label-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/157-shadcn-line-label-custom/tanstack.ts b/benchmarks/conformance/cases/157-shadcn-line-label-custom/tanstack.ts index 49625516..09b64e7e 100644 --- a/benchmarks/conformance/cases/157-shadcn-line-label-custom/tanstack.ts +++ b/benchmarks/conformance/cases/157-shadcn-line-label-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-label-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Custom Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/158-shadcn-line-label/example.tsx b/benchmarks/conformance/cases/158-shadcn-line-label/example.tsx new file mode 100644 index 00000000..be8b0515 --- /dev/null +++ b/benchmarks/conformance/cases/158-shadcn-line-label/example.tsx @@ -0,0 +1,225 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + text, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveNatural } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveNatural + const labelMarks = [ + text(shadcnMonths, { + id: 'visitor-labels', + x: 'month', + y: 'desktop', + text: (row) => row.desktop.toLocaleString('en-US'), + dy: -12, + fill: 'var(--foreground)', + fontSize: 12, + }), + ] + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ...labelMarks, + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 24, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/158-shadcn-line-label/styles.css b/benchmarks/conformance/cases/158-shadcn-line-label/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/158-shadcn-line-label/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/158-shadcn-line-label/tanstack.ts b/benchmarks/conformance/cases/158-shadcn-line-label/tanstack.ts index 430e54b4..f2c4b3f1 100644 --- a/benchmarks/conformance/cases/158-shadcn-line-label/tanstack.ts +++ b/benchmarks/conformance/cases/158-shadcn-line-label/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-label') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx b/benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx new file mode 100644 index 00000000..ca115127 --- /dev/null +++ b/benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx @@ -0,0 +1,212 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinear } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveLinear + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Linear

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/159-shadcn-line-linear/styles.css b/benchmarks/conformance/cases/159-shadcn-line-linear/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/159-shadcn-line-linear/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/159-shadcn-line-linear/tanstack.ts b/benchmarks/conformance/cases/159-shadcn-line-linear/tanstack.ts index 9e3151ce..a2c1a9cf 100644 --- a/benchmarks/conformance/cases/159-shadcn-line-linear/tanstack.ts +++ b/benchmarks/conformance/cases/159-shadcn-line-linear/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-linear') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Linear implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/16-lollipop/example.tsx b/benchmarks/conformance/cases/16-lollipop/example.tsx new file mode 100644 index 00000000..9fab3cca --- /dev/null +++ b/benchmarks/conformance/cases/16-lollipop/example.tsx @@ -0,0 +1,74 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { defineChart, dot, link } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +const percent = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 1, +}) + +export const definition = (_input: ExampleOptions) => { + return defineChart({ + marks: [ + link(alphabet, { + x1: 'letter', + y1: () => 0, + x2: 'letter', + y2: 'frequency', + stroke: '#94a3b8', + strokeWidth: 1.5, + }), + dot(alphabet, { + x: 'letter', + y: 'frequency', + fill: '#2563eb', + r: 4, + }), + ], + x: { + scale: () => scaleBand().padding(0.3), + }, + y: { + scale: scaleLinear, + grid: true, + axis: { + ticks: { format: (value) => percent.format(value) }, + label: 'Frequency', + }, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Ranked lollipop chart' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.letter} · ${percent.format(datum.frequency)} frequency`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/16-lollipop/tanstack.ts b/benchmarks/conformance/cases/16-lollipop/tanstack.ts index e3ca1db4..6c029e65 100644 --- a/benchmarks/conformance/cases/16-lollipop/tanstack.ts +++ b/benchmarks/conformance/cases/16-lollipop/tanstack.ts @@ -1,47 +1,8 @@ -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { defineChart, dot, link } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const percent = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 1, -}) +export * from './example' -const definition = (_input: ConformanceInput) => { - return defineChart({ - marks: [ - link(alphabet, { - x1: 'letter', - y1: () => 0, - x2: 'letter', - y2: 'frequency', - stroke: '#94a3b8', - strokeWidth: 1.5, - }), - dot(alphabet, { - x: 'letter', - y: 'frequency', - fill: '#2563eb', - r: 4, - }), - ], - x: { - scale: () => scaleBand().padding(0.3), - }, - y: { - scale: scaleLinear, - grid: true, - axis: { - ticks: { format: (value) => percent.format(value) }, - label: 'Frequency', - }, - }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount(definition, 'Ranked lollipop chart', { - format: ({ datum }) => - `${datum.letter} · ${percent.format(datum.frequency)} frequency`, -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx b/benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx new file mode 100644 index 00000000..4f091f34 --- /dev/null +++ b/benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx @@ -0,0 +1,214 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveMonotoneX } from 'd3-shape' +import { + shadcnColors, + shadcnSeriesRows, + type ShadcnSeriesDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnSeriesRows.filter((row) => row.series !== 'tablet') + const curve = curveMonotoneX + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnSeriesDatum[], { + id: 'visitor-lines', + x: 'month', + y: 'value', + z: 'series', + color: 'series', + key: (row) => `${row.month}:${row.series}`, + curve: d3Curve(curve), + strokeWidth: 2, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Multiple

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/160-shadcn-line-multiple/styles.css b/benchmarks/conformance/cases/160-shadcn-line-multiple/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/160-shadcn-line-multiple/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/160-shadcn-line-multiple/tanstack.ts b/benchmarks/conformance/cases/160-shadcn-line-multiple/tanstack.ts index 2e6653d6..b7b4531c 100644 --- a/benchmarks/conformance/cases/160-shadcn-line-multiple/tanstack.ts +++ b/benchmarks/conformance/cases/160-shadcn-line-multiple/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-multiple') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Multiple implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/161-shadcn-line-step/example.tsx b/benchmarks/conformance/cases/161-shadcn-line-step/example.tsx new file mode 100644 index 00000000..8c04bed4 --- /dev/null +++ b/benchmarks/conformance/cases/161-shadcn-line-step/example.tsx @@ -0,0 +1,212 @@ +import { useMemo } from 'react' +import { + d3Curve, + defineChart, + lineY, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveStep } from 'd3-shape' +import { + shadcnColors, + shadcnMonths, + type ShadcnMonthDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnMonths + const curve = curveStep + return defineChart(({ width }) => ({ + marks: [ + lineY(rows as readonly ShadcnMonthDatum[], { + id: 'visitor-line', + x: 'month', + y: 'desktop', + curve: d3Curve(curve), + stroke: shadcnColors[0], + strokeWidth: 2, + }), + ], + x: shadcnPointXAxis(), + y: { + scale: scaleLinear().domain([0, 320]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 80, 160, 240, 320], + size: 0, + }, + tickLabels: false, + }, + }, + color: { + domain: twoSeries, + range: shadcnColors.slice(0, 2), + }, + margin: { + top: false && width < 400 ? 16 : 8, + right: 12, + bottom: 35, + left: 12, + }, + theme: shadcnTheme(), + })) +} +function shadcnPointXAxis() { + return { + scale: scalePoint, + axis: { + line: false, + ticks: { + size: 0, + padding: 10, + format: (value: string) => value.slice(0, 3), + }, + }, + } +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Line Chart - Step

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/161-shadcn-line-step/styles.css b/benchmarks/conformance/cases/161-shadcn-line-step/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/161-shadcn-line-step/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/161-shadcn-line-step/tanstack.ts b/benchmarks/conformance/cases/161-shadcn-line-step/tanstack.ts index a2459ec3..3ec224fa 100644 --- a/benchmarks/conformance/cases/161-shadcn-line-step/tanstack.ts +++ b/benchmarks/conformance/cases/161-shadcn-line-step/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-line-step') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Line Chart - Step implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx new file mode 100644 index 00000000..6905967d --- /dev/null +++ b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx @@ -0,0 +1,195 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 5 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: 60, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + radialArc(arcs.slice(0, 1), { + id: 'active-browser-slice', + key: 'browser', + innerRadius: 60, + outerRadius: ({ radius }) => radius + 10, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: 5, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Donut Active

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/styles.css b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/tanstack.ts b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/tanstack.ts index aa254872..21731977 100644 --- a/benchmarks/conformance/cases/162-shadcn-pie-donut-active/tanstack.ts +++ b/benchmarks/conformance/cases/162-shadcn-pie-donut-active/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-donut-active') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Donut Active implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx b/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx new file mode 100644 index 00000000..8c774192 --- /dev/null +++ b/benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx @@ -0,0 +1,186 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: 60, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Donut

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/163-shadcn-pie-donut/styles.css b/benchmarks/conformance/cases/163-shadcn-pie-donut/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/163-shadcn-pie-donut/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/163-shadcn-pie-donut/tanstack.ts b/benchmarks/conformance/cases/163-shadcn-pie-donut/tanstack.ts index 1841429f..05f8bc10 100644 --- a/benchmarks/conformance/cases/163-shadcn-pie-donut/tanstack.ts +++ b/benchmarks/conformance/cases/163-shadcn-pie-donut/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-donut') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Donut implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx b/benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx new file mode 100644 index 00000000..ab0d6246 --- /dev/null +++ b/benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx @@ -0,0 +1,300 @@ +import { useMemo, useState } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { + shadcnColors, + type ShadcnBrowserDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const interactivePieRows: readonly ShadcnBrowserDatum[] = [ + { browser: 'january', visitors: 186 }, + { browser: 'february', visitors: 305 }, + { browser: 'march', visitors: 237 }, + { browser: 'april', visitors: 173 }, + { browser: 'may', visitors: 209 }, +] +function createDefinition(activeMonth = 'january') { + const arcs = pie(interactivePieRows, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const activeIndex = Math.max( + 0, + interactivePieRows.findIndex((row) => row.browser === activeMonth), + ) + const activeRow = interactivePieRows[activeIndex]! + const active = arcs.filter((row) => row.browser === activeRow.browser) + const activeColor = shadcnColors[activeIndex]! + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialArc(arcs, { + id: 'month-slices', + key: 'browser', + innerRadius: 60, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: 1, + }), + radialArc(active, { + id: 'active-month', + key: 'browser', + innerRadius: 60, + outerRadius: ({ radius }) => radius + 10, + fill: activeColor, + stroke: 'var(--background)', + strokeWidth: 5, + }), + radialArc(active, { + id: 'active-month-ring', + key: 'browser', + innerRadius: ({ radius }) => radius + 12, + outerRadius: ({ radius }) => radius + 25, + fill: activeColor, + stroke: 'var(--background)', + strokeWidth: 3, + }), + radialText( + [ + { + id: 'total', + angle: 0, + radius: 0, + text: String(activeRow.visitors), + }, + ], + { + id: 'active-total', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: -5, + fill: 'var(--foreground)', + fontSize: 30, + fontWeight: 700, + }, + ), + radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], { + id: 'active-label', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: 20, + fill: 'var(--muted-foreground)', + fontSize: 12, + }), + ], + }), + ], + color: { + domain: interactivePieRows.map((row) => row.browser), + range: shadcnColors, + }, + margin: 0, + }) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart(activeMonth: string = 'january') { + return defineChart(createDefinition(activeMonth), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const [activeMonth, setActiveMonth] = useState('january') + const chartDefinition = useMemo( + () => createExampleChart(activeMonth), + [activeMonth], + ) + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(300, contentWidth) + const chartHeight = chartWidth + const headerAction = ( + ({ + value: row.browser, + label: titleCase(row.browser), + swatch: shadcnColors[index], + }))} + /> + ) + const legend = null + const footer = null + return ( +
+
+
+
+

Pie Chart - Interactive

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function SelectControl({ + value, + options, + onChange, +}: { + value: string + options: readonly { + value: string + label: string + swatch?: string + }[] + onChange: (value: string) => void +}) { + const selected = options.find((option) => option.value === value) + return ( + + ) +} diff --git a/benchmarks/conformance/cases/164-shadcn-pie-interactive/styles.css b/benchmarks/conformance/cases/164-shadcn-pie-interactive/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/164-shadcn-pie-interactive/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/164-shadcn-pie-interactive/tanstack.ts b/benchmarks/conformance/cases/164-shadcn-pie-interactive/tanstack.ts index 2a4b6e69..aa41c8aa 100644 --- a/benchmarks/conformance/cases/164-shadcn-pie-interactive/tanstack.ts +++ b/benchmarks/conformance/cases/164-shadcn-pie-interactive/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-interactive') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Interactive implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx new file mode 100644 index 00000000..8fad7cbc --- /dev/null +++ b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx @@ -0,0 +1,204 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + radialText(arcs, { + id: 'browser-labels', + angle: 'angle', + radius: 1, + radiusOffset: 17, + text: (row) => row.visitors.toLocaleString('en-US'), + key: 'browser', + anchor: 'outside', + fill: 'var(--foreground)', + fontSize: 12, + fontWeight: undefined, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Custom Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/165-shadcn-pie-label-custom/styles.css b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/165-shadcn-pie-label-custom/tanstack.ts b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/tanstack.ts index 9cc97793..660c5d9e 100644 --- a/benchmarks/conformance/cases/165-shadcn-pie-label-custom/tanstack.ts +++ b/benchmarks/conformance/cases/165-shadcn-pie-label-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-label-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Custom Label implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx b/benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx new file mode 100644 index 00000000..46c0afc9 --- /dev/null +++ b/benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx @@ -0,0 +1,204 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + radialText(arcs, { + id: 'browser-labels', + angle: 'angle', + radius: 0.68, + radiusOffset: 0, + text: (row) => titleCase(row.browser), + key: 'browser', + anchor: 'middle', + fill: 'var(--background)', + fontSize: 12, + fontWeight: 400, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Label List

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/166-shadcn-pie-label-list/styles.css b/benchmarks/conformance/cases/166-shadcn-pie-label-list/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/166-shadcn-pie-label-list/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/166-shadcn-pie-label-list/tanstack.ts b/benchmarks/conformance/cases/166-shadcn-pie-label-list/tanstack.ts index 98ef087b..411cab41 100644 --- a/benchmarks/conformance/cases/166-shadcn-pie-label-list/tanstack.ts +++ b/benchmarks/conformance/cases/166-shadcn-pie-label-list/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-label-list') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Label List implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx b/benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx new file mode 100644 index 00000000..decb7c10 --- /dev/null +++ b/benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx @@ -0,0 +1,214 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialRule, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + radialRule(arcs, { + id: 'browser-leaders', + angle: 'angle', + radius1: 1, + radius2: 1, + radius2Offset: 14, + key: 'browser', + stroke: 'var(--border)', + }), + radialText(arcs, { + id: 'browser-labels', + angle: 'angle', + radius: 1, + radiusOffset: 17, + text: (row) => row.visitors.toLocaleString('en-US'), + key: 'browser', + anchor: 'outside', + fill: 'var(--foreground)', + fontSize: 12, + fontWeight: undefined, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/167-shadcn-pie-label/styles.css b/benchmarks/conformance/cases/167-shadcn-pie-label/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/167-shadcn-pie-label/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/167-shadcn-pie-label/tanstack.ts b/benchmarks/conformance/cases/167-shadcn-pie-label/tanstack.ts index 0ac2dbe4..f1dc1e9e 100644 --- a/benchmarks/conformance/cases/167-shadcn-pie-label/tanstack.ts +++ b/benchmarks/conformance/cases/167-shadcn-pie-label/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-label') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Label implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx b/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx new file mode 100644 index 00000000..340ff746 --- /dev/null +++ b/benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx @@ -0,0 +1,185 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = + const footer = null + return ( +
+
+
+
+

Pie Chart - Legend

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function Legend() { + return ( + <> + {['chrome', 'safari', 'firefox', 'edge', 'other'].map((label, index) => ( + + + {titleCase(label)} + + ))} + + ) +} diff --git a/benchmarks/conformance/cases/168-shadcn-pie-legend/styles.css b/benchmarks/conformance/cases/168-shadcn-pie-legend/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/168-shadcn-pie-legend/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/168-shadcn-pie-legend/tanstack.ts b/benchmarks/conformance/cases/168-shadcn-pie-legend/tanstack.ts index dcfeb277..67a884d3 100644 --- a/benchmarks/conformance/cases/168-shadcn-pie-legend/tanstack.ts +++ b/benchmarks/conformance/cases/168-shadcn-pie-legend/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-legend') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Legend implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx new file mode 100644 index 00000000..1f402121 --- /dev/null +++ b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx @@ -0,0 +1,186 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 0 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Separator None

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/styles.css b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/tanstack.ts b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/tanstack.ts index f11323b7..a982baee 100644 --- a/benchmarks/conformance/cases/169-shadcn-pie-separator-none/tanstack.ts +++ b/benchmarks/conformance/cases/169-shadcn-pie-separator-none/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-separator-none') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Separator None implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/17-dumbbell/example.tsx b/benchmarks/conformance/cases/17-dumbbell/example.tsx new file mode 100644 index 00000000..97064057 --- /dev/null +++ b/benchmarks/conformance/cases/17-dumbbell/example.tsx @@ -0,0 +1,75 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { citywages } from '@charts-poc/demo-data/citywages' +import { defineChart, dot, link } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +export const definition = (input: ExampleOptions) => { + const rows = citywages.slice(input.revision * 4, input.revision * 4 + 8) + return defineChart({ + marks: [ + link(rows, { + x1: 'R90_10_1980', + y1: 'nyt_display', + x2: 'R90_10_2015', + y2: 'nyt_display', + stroke: '#94a3b8', + strokeWidth: 2, + }), + dot(rows, { + x: 'R90_10_1980', + y: 'nyt_display', + fill: '#2563eb', + r: 4, + }), + dot(rows, { + x: 'R90_10_2015', + y: 'nyt_display', + fill: '#f97316', + r: 4, + }), + ], + x: { + scale: scaleLinear, + grid: true, + axis: { label: '90th/10th percentile wage ratio' }, + }, + y: { + scale: () => scaleBand().padding(0.22), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Metropolitan wage inequality in 1980 and 2015' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.nyt_display} · 1980 ${datum.R90_10_1980.toLocaleString( + 'en-US', + )} · 2015 ${datum.R90_10_2015.toLocaleString('en-US')}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/17-dumbbell/tanstack.ts b/benchmarks/conformance/cases/17-dumbbell/tanstack.ts index 41575d4d..6c029e65 100644 --- a/benchmarks/conformance/cases/17-dumbbell/tanstack.ts +++ b/benchmarks/conformance/cases/17-dumbbell/tanstack.ts @@ -1,52 +1,8 @@ -import { citywages } from '@charts-poc/demo-data/citywages' -import { defineChart, dot, link } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => { - const rows = citywages.slice(input.revision * 4, input.revision * 4 + 8) - return defineChart({ - marks: [ - link(rows, { - x1: 'R90_10_1980', - y1: 'nyt_display', - x2: 'R90_10_2015', - y2: 'nyt_display', - stroke: '#94a3b8', - strokeWidth: 2, - }), - dot(rows, { - x: 'R90_10_1980', - y: 'nyt_display', - fill: '#2563eb', - r: 4, - }), - dot(rows, { - x: 'R90_10_2015', - y: 'nyt_display', - fill: '#f97316', - r: 4, - }), - ], - x: { - scale: scaleLinear, - grid: true, - axis: { label: '90th/10th percentile wage ratio' }, - }, - y: { - scale: () => scaleBand().padding(0.22), - }, - }) -} +export * from './example' -export const mount = tanstackMount( - definition, - 'Metropolitan wage inequality in 1980 and 2015', - { - format: ({ datum }) => - `${datum.nyt_display} · 1980 ${datum.R90_10_1980.toLocaleString( - 'en-US', - )} · 2015 ${datum.R90_10_2015.toLocaleString('en-US')}`, - }, -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx b/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx new file mode 100644 index 00000000..14c5fe72 --- /dev/null +++ b/benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx @@ -0,0 +1,186 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const arcs = pie(shadcnBrowsers, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const separator = 1 + const marks = [ + radialArc(arcs, { + id: 'browser-slices', + key: 'browser', + innerRadius: undefined, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: separator, + }), + ] + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.78, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks, + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/170-shadcn-pie-simple/styles.css b/benchmarks/conformance/cases/170-shadcn-pie-simple/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/170-shadcn-pie-simple/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/170-shadcn-pie-simple/tanstack.ts b/benchmarks/conformance/cases/170-shadcn-pie-simple/tanstack.ts index 7316ddbc..9ab8569e 100644 --- a/benchmarks/conformance/cases/170-shadcn-pie-simple/tanstack.ts +++ b/benchmarks/conformance/cases/170-shadcn-pie-simple/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-simple') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx b/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx new file mode 100644 index 00000000..0438763c --- /dev/null +++ b/benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx @@ -0,0 +1,209 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, pie, polar, radialArc } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear } from 'd3-scale' +import { shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + return buildStackedPieDefinition() +} +function buildStackedPieDefinition() { + const desktopRows = [186, 305, 237, 173, 209].map((visitors, index) => ({ + browser: ['january', 'february', 'march', 'april', 'may'][index]!, + visitors, + })) + const mobileRows = [80, 200, 120, 190, 130].map((visitors, index) => ({ + browser: ['january', 'february', 'march', 'april', 'may'][index]!, + visitors, + })) + const desktopArcs = pie(desktopRows, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const mobileArcs = pie(mobileRows, { + value: 'visitors', + startAngle: Math.PI / 2, + endAngle: (-Math.PI * 3) / 2, + }) + const months = desktopRows.map((row) => row.browser) + return defineChart({ + marks: [ + polar({ + radiusRatio: 1, + angle: { scale: scaleLinear().domain([0, Math.PI * 2]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialArc(desktopArcs, { + id: 'desktop-slices', + key: 'browser', + outerRadius: 60, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: 1, + }), + radialArc(mobileArcs, { + id: 'mobile-slices', + key: 'browser', + innerRadius: 70, + outerRadius: 90, + color: 'browser', + stroke: 'var(--background)', + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: months, range: shadcnColors }, + margin: 0, + }) +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Pie Chart - Stacked

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/171-shadcn-pie-stacked/styles.css b/benchmarks/conformance/cases/171-shadcn-pie-stacked/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/171-shadcn-pie-stacked/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/171-shadcn-pie-stacked/tanstack.ts b/benchmarks/conformance/cases/171-shadcn-pie-stacked/tanstack.ts index af81601e..bc9df805 100644 --- a/benchmarks/conformance/cases/171-shadcn-pie-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/171-shadcn-pie-stacked/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-pie-stacked') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Pie Chart - Stacked implemented with TanStack Charts', + { guides: false, margin: true }, +) diff --git a/benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx b/benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx new file mode 100644 index 00000000..8f82f491 --- /dev/null +++ b/benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx @@ -0,0 +1,210 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/172-shadcn-radar-default/styles.css b/benchmarks/conformance/cases/172-shadcn-radar-default/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/172-shadcn-radar-default/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/172-shadcn-radar-default/tanstack.ts b/benchmarks/conformance/cases/172-shadcn-radar-default/tanstack.ts index b618d1b6..8de58daa 100644 --- a/benchmarks/conformance/cases/172-shadcn-radar-default/tanstack.ts +++ b/benchmarks/conformance/cases/172-shadcn-radar-default/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-default') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx b/benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx new file mode 100644 index 00000000..97969703 --- /dev/null +++ b/benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx @@ -0,0 +1,220 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialDot, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialDot(rows, { + id: 'radar-dots', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + r: 4, + fill: shadcnColors[0], + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Dots

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/173-shadcn-radar-dots/styles.css b/benchmarks/conformance/cases/173-shadcn-radar-dots/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/173-shadcn-radar-dots/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/173-shadcn-radar-dots/tanstack.ts b/benchmarks/conformance/cases/173-shadcn-radar-dots/tanstack.ts index 77b53490..bf86d2f8 100644 --- a/benchmarks/conformance/cases/173-shadcn-radar-dots/tanstack.ts +++ b/benchmarks/conformance/cases/173-shadcn-radar-dots/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-dots') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Dots implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx new file mode 100644 index 00000000..551025ce --- /dev/null +++ b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx @@ -0,0 +1,210 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarFilled } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarFilled + const months = rows.map((row) => row.month) + const radiusMax = 300 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'circle', + stroke: 'transparent', + strokeOpacity: 0, + ...{ fill: shadcnColors[0], fillOpacity: 0.2 }, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 0, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.5, + stroke: shadcnColors[0], + strokeOpacity: 0, + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid Circle Filled

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/styles.css b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/tanstack.ts b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/tanstack.ts index c2958d4e..73704643 100644 --- a/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/tanstack.ts +++ b/benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-circle-fill') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid Circle Filled implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx new file mode 100644 index 00000000..b17e6d3f --- /dev/null +++ b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx @@ -0,0 +1,222 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialDot, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault.map((row) => + row.month === 'April' ? { ...row, desktop: 203 } : row, + ) + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'circle', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialDot(rows, { + id: 'radar-dots', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + r: 4, + fill: shadcnColors[0], + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid Circle - No lines

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/styles.css b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts index 20b63ad7..e4e145d3 100644 --- a/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts +++ b/benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-circle-no-lines') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid Circle - No lines implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx new file mode 100644 index 00000000..9015acb3 --- /dev/null +++ b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx @@ -0,0 +1,220 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialDot, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'circle', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialDot(rows, { + id: 'radar-dots', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + r: 4, + fill: shadcnColors[0], + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid Circle

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/styles.css b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/tanstack.ts b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/tanstack.ts index 273d3146..baacdce8 100644 --- a/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/tanstack.ts +++ b/benchmarks/conformance/cases/176-shadcn-radar-grid-circle/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-circle') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid Circle implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx new file mode 100644 index 00000000..6e1f3a9b --- /dev/null +++ b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx @@ -0,0 +1,207 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault + const months = rows.map((row) => row.month) + const radiusMax = 320 + const customLabels = false + const guides = [ + radialGrid({ + values: [Math.round(radiusMax * 0.947)], + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid Custom

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/styles.css b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/tanstack.ts b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/tanstack.ts index 38216636..c68b8d24 100644 --- a/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/tanstack.ts +++ b/benchmarks/conformance/cases/177-shadcn-radar-grid-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid Custom implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx new file mode 100644 index 00000000..4cadba27 --- /dev/null +++ b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx @@ -0,0 +1,210 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarFilled } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarFilled + const months = rows.map((row) => row.month) + const radiusMax = 300 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'transparent', + strokeOpacity: 0, + ...{ fill: shadcnColors[0], fillOpacity: 0.2 }, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 0, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.5, + stroke: shadcnColors[0], + strokeOpacity: 0, + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid Filled

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/styles.css b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/tanstack.ts b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/tanstack.ts index 9b0f3286..dc7e9bc7 100644 --- a/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/tanstack.ts +++ b/benchmarks/conformance/cases/178-shadcn-radar-grid-fill/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-fill') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid Filled implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx new file mode 100644 index 00000000..02f3e29d --- /dev/null +++ b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx @@ -0,0 +1,208 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialDot, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarDefault } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarDefault + const months = rows.map((row) => row.month) + const radiusMax = 320 + const customLabels = false + const guides = [ + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialDot(rows, { + id: 'radar-dots', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + r: 4, + fill: shadcnColors[0], + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Grid None

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/179-shadcn-radar-grid-none/styles.css b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/179-shadcn-radar-grid-none/tanstack.ts b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/tanstack.ts index 6bbb28b8..7fe418ab 100644 --- a/benchmarks/conformance/cases/179-shadcn-radar-grid-none/tanstack.ts +++ b/benchmarks/conformance/cases/179-shadcn-radar-grid-none/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-grid-none') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Grid None implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/18-cumulative-histogram/example.tsx b/benchmarks/conformance/cases/18-cumulative-histogram/example.tsx new file mode 100644 index 00000000..9f09e73f --- /dev/null +++ b/benchmarks/conformance/cases/18-cumulative-histogram/example.tsx @@ -0,0 +1,65 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { olympians } from '@charts-poc/demo-data/olympians' +import { binX, cumulative, defineChart, rect } from '@tanstack/charts' +import { thresholdScott } from 'd3-array' +import { scaleLinear } from 'd3-scale' +import type { OlympiansRow } from '@charts-poc/demo-data/olympians' + +type OlympianWithWeight = OlympiansRow & { weight: number } + +const completeOlympians = olympians.filter( + (row): row is OlympianWithWeight => row.weight !== null, +) +export const definition = (input: ExampleOptions) => { + const bins = binX(completeOlympians.slice(input.revision * 8), { + value: 'weight', + thresholds: thresholdScott, + outputs: { count: { reduce: 'count' } }, + }) + const cumulativeBins = cumulative(bins, { + orderBy: 'x1', + outputs: { cumulativeCount: { value: 'count', reduce: 'sum' } }, + }) + + return defineChart({ + marks: [ + rect(cumulativeBins, { + x1: 'x1', + x2: 'x2', + y1: () => 0, + y2: 'cumulativeCount', + fill: '#2563eb', + inset: 1, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (kg)' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Cumulative count' } }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Cumulative histogram' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts b/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts index a63eab8c..6c029e65 100644 --- a/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts +++ b/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts @@ -1,41 +1,8 @@ -import { olympians } from '@charts-poc/demo-data/olympians' -import { binX, cumulative, defineChart, rect } from '@tanstack/charts' -import { thresholdScott } from 'd3-array' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { OlympiansRow } from '@charts-poc/demo-data/olympians' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type OlympianWithWeight = OlympiansRow & { weight: number } +export * from './example' -const completeOlympians = olympians.filter( - (row): row is OlympianWithWeight => row.weight !== null, -) -const definition = (input: ConformanceInput) => { - const bins = binX(completeOlympians.slice(input.revision * 8), { - value: 'weight', - thresholds: thresholdScott, - outputs: { count: { reduce: 'count' } }, - }) - const cumulativeBins = cumulative(bins, { - orderBy: 'x1', - outputs: { cumulativeCount: { value: 'count', reduce: 'sum' } }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - rect(cumulativeBins, { - x1: 'x1', - x2: 'x2', - y1: () => 0, - y2: 'cumulativeCount', - fill: '#2563eb', - inset: 1, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (kg)' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Cumulative count' } }, - }) -} - -export const mount = tanstackMount(definition, 'Cumulative histogram') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx b/benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx new file mode 100644 index 00000000..7526d58b --- /dev/null +++ b/benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx @@ -0,0 +1,243 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarMultiple } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarMultiple + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 1, + stroke: shadcnColors[1], + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = + const footer = + return ( +
+
+
+
+

Radar Chart - Icons

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + + + {titleCase(label)} + + ))} + + ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/180-shadcn-radar-icons/styles.css b/benchmarks/conformance/cases/180-shadcn-radar-icons/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/180-shadcn-radar-icons/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/180-shadcn-radar-icons/tanstack.ts b/benchmarks/conformance/cases/180-shadcn-radar-icons/tanstack.ts index 02a0e031..fbf5489a 100644 --- a/benchmarks/conformance/cases/180-shadcn-radar-icons/tanstack.ts +++ b/benchmarks/conformance/cases/180-shadcn-radar-icons/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-icons') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Icons implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx new file mode 100644 index 00000000..c7abc5f7 --- /dev/null +++ b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx @@ -0,0 +1,251 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarMultiple } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarMultiple + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = true + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 1, + stroke: shadcnColors[1], + strokeWidth: 1, + }), + radialText(rows, { + id: 'radar-values', + angle: 'month', + radius: () => radiusMax, + key: 'month', + z: () => 'desktop', + text: (row) => `${row.desktop}/${row.mobile ?? 0}`, + anchor: 'outside', + radiusOffset: 15, + dy: -7, + fill: 'var(--foreground)', + fontSize: 12, + fontWeight: 500, + }), + radialText(rows, { + id: 'radar-months', + angle: 'month', + radius: () => radiusMax, + key: 'month', + z: () => 'desktop', + text: 'month', + anchor: 'outside', + radiusOffset: 15, + dy: 10, + fill: 'var(--muted-foreground)', + fontSize: 12, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(({ height }) => chartSpec(height < 220 ? 0.64 : 0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Custom Label

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/styles.css b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/tanstack.ts b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/tanstack.ts index b82fc1e7..a852315f 100644 --- a/benchmarks/conformance/cases/181-shadcn-radar-label-custom/tanstack.ts +++ b/benchmarks/conformance/cases/181-shadcn-radar-label-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-label-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Custom Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx b/benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx new file mode 100644 index 00000000..245b8422 --- /dev/null +++ b/benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx @@ -0,0 +1,238 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarMultiple } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarMultiple + const months = rows.map((row) => row.month) + const radiusMax = 320 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'var(--border)', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 1, + stroke: shadcnColors[1], + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = + const footer = + return ( +
+
+
+
+

Radar Chart - Legend

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + {titleCase(label)} + + ))} + + ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/182-shadcn-radar-legend/styles.css b/benchmarks/conformance/cases/182-shadcn-radar-legend/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/182-shadcn-radar-legend/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/182-shadcn-radar-legend/tanstack.ts b/benchmarks/conformance/cases/182-shadcn-radar-legend/tanstack.ts index 21d0c0ac..e528a9e8 100644 --- a/benchmarks/conformance/cases/182-shadcn-radar-legend/tanstack.ts +++ b/benchmarks/conformance/cases/182-shadcn-radar-legend/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-legend') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Legend implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx new file mode 100644 index 00000000..755a992c --- /dev/null +++ b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx @@ -0,0 +1,223 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarLines } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarLines + const months = rows.map((row) => row.month) + const radiusMax = 220 + const gridValues = Array.from({ length: 4 }, (_, index) => + Math.round((radiusMax * (index + 1)) / 4), + ) + const customLabels = false + const guides = [ + radialGrid({ + values: gridValues, + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{}, + }), + angleGrid({ + values: months, + labels: !customLabels, + labelOffset: 10, + labelFill: 'var(--muted-foreground)', + labelFontSize: 12, + stroke: 'transparent', + strokeOpacity: 1, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 2, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 0, + stroke: shadcnColors[1], + strokeWidth: 2, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Lines Only

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/183-shadcn-radar-lines-only/styles.css b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/183-shadcn-radar-lines-only/tanstack.ts b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/tanstack.ts index 2568e8a7..7978393c 100644 --- a/benchmarks/conformance/cases/183-shadcn-radar-lines-only/tanstack.ts +++ b/benchmarks/conformance/cases/183-shadcn-radar-lines-only/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-lines-only') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Lines Only implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx b/benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx new file mode 100644 index 00000000..f4346dae --- /dev/null +++ b/benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx @@ -0,0 +1,216 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { shadcnColors, shadcnRadarMultiple } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + const rows = shadcnRadarMultiple + const months = rows.map((row) => row.month) + const radiusMax = 320 + const guides = [ + radialGrid({ + values: [0, 80, 160, 240, 320], + shape: 'polygon', + stroke: 'var(--border)', + strokeOpacity: 1, + ...{}, + ...{ + labels: true, + labelAngle: Math.PI / 6, + labelFill: 'var(--foreground)', + labelFontSize: 12, + labelRotate: 30, + labelOffset: 1, + }, + }), + ] + const chartSpec = (radiusRatio: number) => ({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scalePoint().domain(months), wrap: true }, + radius: { scale: scaleLinear().domain([0, radiusMax]) }, + guides, + marks: [ + radialArea(rows, { + id: 'desktop-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'desktop', + key: 'month', + z: () => 'desktop', + curve: curveLinearClosed, + fill: shadcnColors[0], + fillOpacity: 0.6, + stroke: shadcnColors[0], + strokeOpacity: 1, + strokeWidth: 1, + }), + radialArea(rows, { + id: 'mobile-radar', + className: 'ts-chart__radar', + angle: 'month', + radius: 'mobile', + key: 'month', + z: () => 'mobile', + curve: curveLinearClosed, + fill: shadcnColors[1], + fillOpacity: 1, + stroke: shadcnColors[1], + strokeWidth: 1, + }), + ], + }), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) + return defineChart(chartSpec(0.76)) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + return { + title: String(points[0]?.xValue ?? ''), + rows: points.map((point) => ({ + label: titleCase( + String( + point.group ?? + point.markId.replace( + /-?(bars|lines|areas|slices|values|radar)$/u, + '', + ), + ), + ), + value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'), + color: point.color, + })), + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = + return ( +
+
+
+
+

Radar Chart - Radius Axis

+

Showing total visitors for the last 6 months

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/184-shadcn-radar-radius/styles.css b/benchmarks/conformance/cases/184-shadcn-radar-radius/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/184-shadcn-radar-radius/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/184-shadcn-radar-radius/tanstack.ts b/benchmarks/conformance/cases/184-shadcn-radar-radius/tanstack.ts index 494ae63e..67976f63 100644 --- a/benchmarks/conformance/cases/184-shadcn-radar-radius/tanstack.ts +++ b/benchmarks/conformance/cases/184-shadcn-radar-radius/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radar-radius') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radar Chart - Radius Axis implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx b/benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx new file mode 100644 index 00000000..b4b1f401 --- /dev/null +++ b/benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx @@ -0,0 +1,207 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + angleGrid, + focusGroupAngle, + polar, + radialBarAngle, + radialGrid, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const startAngle = rechartsPolarAngle(0) + const endAngle = rechartsPolarAngle(360) + const chartSpec = (radiusFactor: number) => ({ + marks: [ + polar({ + radiusRatio: 0.8, + startAngle, + endAngle, + angle: { scale: scaleLinear().domain([0, 300]) }, + radius: { + scale: scaleBand().domain(browserNames).paddingInner(0.2), + range: [30 * radiusFactor, 100 * radiusFactor], + }, + guides: [ + radialGrid({ + values: browserNames, + shape: 'circle', + stroke: 'var(--border)', + }), + angleGrid({ + values: Array.from({ length: 12 }, (_, index) => index * 25), + labels: false, + stroke: 'var(--border)', + }), + ], + marks: [ + radialBarAngle(shadcnBrowsers, { + id: 'radial-values', + angle: 'visitors', + radius: 'browser', + key: 'browser', + fill: (row) => shadcnColors[browserNames.indexOf(row.browser)]!, + }), + ], + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) + return defineChart(chartSpec(1)) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart - Grid

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/185-shadcn-radial-grid/styles.css b/benchmarks/conformance/cases/185-shadcn-radial-grid/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/185-shadcn-radial-grid/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/185-shadcn-radial-grid/tanstack.ts b/benchmarks/conformance/cases/185-shadcn-radial-grid/tanstack.ts index b6a5d637..37a5b0b5 100644 --- a/benchmarks/conformance/cases/185-shadcn-radial-grid/tanstack.ts +++ b/benchmarks/conformance/cases/185-shadcn-radial-grid/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radial-grid') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart - Grid implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx b/benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx new file mode 100644 index 00000000..ea78f9ff --- /dev/null +++ b/benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx @@ -0,0 +1,234 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + polar, + radialBarAngle, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const startAngle = rechartsPolarAngle(-90) + const endAngle = rechartsPolarAngle(380) + const backgroundRows = shadcnBrowsers.map((row) => ({ + ...row, + background: 300, + })) + const chartSpec = (radiusFactor: number) => ({ + marks: [ + polar({ + radiusRatio: 1, + startAngle, + endAngle, + angle: { scale: scaleLinear().domain([0, 300]) }, + radius: { + scale: scaleBand().domain(browserNames).paddingInner(0.2), + range: [30 * radiusFactor, 110 * radiusFactor], + }, + guides: [], + marks: [ + radialBarAngle(backgroundRows, { + id: 'radial-backgrounds', + angle: 'background', + radius: 'browser', + key: 'browser', + fill: 'var(--muted)', + }), + radialBarAngle(shadcnBrowsers, { + id: 'radial-values', + angle: 'visitors', + radius: 'browser', + key: 'browser', + fill: (row) => shadcnColors[browserNames.indexOf(row.browser)]!, + }), + ], + }), + polar({ + startAngle, + endAngle, + angle: { scale: scaleLinear().domain([0, 300]) }, + radius: { + scale: scaleLinear().domain([0, 110]), + range: [0, 110 * radiusFactor], + }, + marks: [ + radialText( + shadcnBrowsers.map((row, index) => ({ + ...row, + angle: 10, + labelRadius: 38 + index * 16, + })), + { + id: 'radial-labels', + angle: 'angle', + radius: 'labelRadius', + key: 'browser', + text: (row) => titleCase(row.browser), + fill: 'white', + fontSize: 11, + rotate: -25, + }, + ), + ], + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) + return defineChart(({ height }) => chartSpec(height < 220 ? 0.8 : 1)) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart - Label

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/186-shadcn-radial-label/styles.css b/benchmarks/conformance/cases/186-shadcn-radial-label/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/186-shadcn-radial-label/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/186-shadcn-radial-label/tanstack.ts b/benchmarks/conformance/cases/186-shadcn-radial-label/tanstack.ts index d579bd32..91ecf90a 100644 --- a/benchmarks/conformance/cases/186-shadcn-radial-label/tanstack.ts +++ b/benchmarks/conformance/cases/186-shadcn-radial-label/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radial-label') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart - Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx b/benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx new file mode 100644 index 00000000..e46f7817 --- /dev/null +++ b/benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx @@ -0,0 +1,264 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + pie, + polar, + radialArc, + radialBarAngle, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +function createDefinition() { + return buildSingleRadialDefinition({ + id: 'shape', + visitors: 1260, + endDegrees: 100, + innerRadius: 65, + outerRadius: 95, + backgroundInnerRadius: 74, + backgroundOuterRadius: 86, + cornerRadius: 0, + total: '1,260', + }) +} +function buildSingleRadialDefinition(options: { + id: string + visitors: number + endDegrees: number + innerRadius: number + outerRadius: number + backgroundInnerRadius: number + backgroundOuterRadius: number + cornerRadius: number + total: string +}) { + const rows = [ + { browser: 'safari', visitors: options.visitors, ring: 'visitors' }, + ] + const background = pie([{ id: 'background', value: 1 }], { value: 'value' }) + return defineChart({ + marks: [ + polar({ + marks: [ + radialArc(background, { + id: `${options.id}-background`, + key: 'id', + innerRadius: options.backgroundInnerRadius, + outerRadius: options.backgroundOuterRadius, + fill: 'var(--muted)', + }), + ], + }), + polar({ + startAngle: rechartsPolarAngle(0), + endAngle: rechartsPolarAngle(options.endDegrees), + angle: { scale: scaleLinear().domain([0, options.visitors]) }, + radius: { + scale: scaleBand().domain(['visitors']), + range: [options.innerRadius, options.outerRadius], + }, + marks: [ + radialBarAngle(rows, { + id: `${options.id}-value`, + angle: 'visitors', + radius: 'ring', + key: 'browser', + fill: shadcnColors[1], + cornerRadius: options.cornerRadius, + }), + ], + }), + radialCenterLabels(options.total, 36, 0, 24), + ], + margin: 0, + }) +} +function radialCenterLabels( + total: string, + fontSize: number, + totalDy: number, + labelDy: number, +) { + return polar({ + angle: { scale: scaleLinear().domain([0, 1]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialText([{ id: 'total', angle: 0, radius: 0, text: total }], { + id: `radial-total-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: totalDy, + fill: 'var(--foreground)', + fontSize, + fontWeight: 700, + }), + radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], { + id: `radial-label-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: labelDy, + fill: 'var(--muted-foreground)', + fontSize: 14, + }), + ], + }) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart - Shape

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/187-shadcn-radial-shape/styles.css b/benchmarks/conformance/cases/187-shadcn-radial-shape/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/187-shadcn-radial-shape/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/187-shadcn-radial-shape/tanstack.ts b/benchmarks/conformance/cases/187-shadcn-radial-shape/tanstack.ts index c6592ae8..8f448cad 100644 --- a/benchmarks/conformance/cases/187-shadcn-radial-shape/tanstack.ts +++ b/benchmarks/conformance/cases/187-shadcn-radial-shape/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radial-shape') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart - Shape implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx b/benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx new file mode 100644 index 00000000..1e272f81 --- /dev/null +++ b/benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { focusGroupAngle, polar, radialBarAngle } from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnBrowsers, shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const browserNames = shadcnBrowsers.map((row) => row.browser) +function createDefinition() { + const startAngle = rechartsPolarAngle(0) + const endAngle = rechartsPolarAngle(360) + const backgroundRows = shadcnBrowsers.map((row) => ({ + ...row, + background: 300, + })) + const chartSpec = (radiusFactor: number) => ({ + marks: [ + polar({ + radiusRatio: 1, + startAngle, + endAngle, + angle: { scale: scaleLinear().domain([0, 300]) }, + radius: { + scale: scaleBand().domain(browserNames).paddingInner(0.2), + range: [30 * radiusFactor, 110 * radiusFactor], + }, + guides: [], + marks: [ + radialBarAngle(backgroundRows, { + id: 'radial-backgrounds', + angle: 'background', + radius: 'browser', + key: 'browser', + fill: 'var(--muted)', + }), + radialBarAngle(shadcnBrowsers, { + id: 'radial-values', + angle: 'visitors', + radius: 'browser', + key: 'browser', + fill: (row) => shadcnColors[browserNames.indexOf(row.browser)]!, + }), + ], + }), + ], + color: { domain: browserNames, range: shadcnColors }, + margin: 0, + }) + return defineChart(chartSpec(1)) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/188-shadcn-radial-simple/styles.css b/benchmarks/conformance/cases/188-shadcn-radial-simple/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/188-shadcn-radial-simple/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/188-shadcn-radial-simple/tanstack.ts b/benchmarks/conformance/cases/188-shadcn-radial-simple/tanstack.ts index c49c5783..b8e788be 100644 --- a/benchmarks/conformance/cases/188-shadcn-radial-simple/tanstack.ts +++ b/benchmarks/conformance/cases/188-shadcn-radial-simple/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radial-simple') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx b/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx new file mode 100644 index 00000000..ab59078d --- /dev/null +++ b/benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx @@ -0,0 +1,249 @@ +import { useMemo } from 'react' +import { + defineChart, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { + focusGroupAngle, + polar, + radialBarAngle, + radialText, +} from '@tanstack/charts/polar' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { shadcnColors } from '@charts-poc/demo-data/shadcn' +import './styles.css' +const twoSeries = ['desktop', 'mobile'] as const +function createDefinition() { + return buildStackedRadialDefinition() +} +function buildStackedRadialDefinition() { + const rows = [ + { + id: 'mobile', + ring: 'visitors', + start: 0, + end: 570, + fill: shadcnColors[1], + }, + { + id: 'desktop', + ring: 'visitors', + start: 570, + end: 1260, + fill: shadcnColors[0], + }, + ] + return defineChart({ + marks: [ + polar({ + startAngle: rechartsPolarAngle(0), + endAngle: rechartsPolarAngle(180), + angle: { scale: scaleLinear().domain([0, 1260]) }, + radius: { + scale: scaleBand().domain(['visitors']), + range: [80, 110], + }, + marks: [ + radialBarAngle(rows, { + id: 'stacked-values', + angle1: 'start', + angle2: 'end', + angle: 'end', + radius: 'ring', + key: 'id', + fill: (row) => row.fill, + cornerRadius: 5, + stroke: 'transparent', + strokeWidth: 2, + }), + ], + }), + radialCenterLabels('1,830', 24, -16, 4), + ], + color: { domain: twoSeries, range: shadcnColors.slice(0, 2) }, + margin: 0, + }) +} +function radialCenterLabels( + total: string, + fontSize: number, + totalDy: number, + labelDy: number, +) { + return polar({ + angle: { scale: scaleLinear().domain([0, 1]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialText([{ id: 'total', angle: 0, radius: 0, text: total }], { + id: `radial-total-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: totalDy, + fill: 'var(--foreground)', + fontSize, + fontWeight: 700, + }), + radialText([{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }], { + id: `radial-label-${total}`, + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + dy: labelDy, + fill: 'var(--muted-foreground)', + fontSize: 14, + }), + ], + }) +} +function rechartsPolarAngle(degrees: number) { + return ((90 - degrees) * Math.PI) / 180 +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function shadcnTooltipContent(points: readonly ChartPoint[]) { + const point = points.find((candidate) => browserMetric(candidate.datum)) + const metric = point && browserMetric(point.datum) + return metric + ? { + title: titleCase(metric.browser), + rows: [ + { + label: 'Visitors', + value: metric.visitors.toLocaleString('en-US'), + color: point.color, + }, + ], + } + : { rows: [] } +} +function browserMetric(datum: unknown) { + if (!datum || typeof datum !== 'object') return undefined + const browser = Reflect.get(datum, 'browser') + const visitors = Reflect.get(datum, 'visitors') + return typeof browser === 'string' && + typeof visitors === 'number' && + Number.isFinite(visitors) + ? { browser, visitors } + : undefined +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: focusGroupAngle, + keyboard: false, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: 'group-center', + placement: 'auto', + offset: undefined, + sort: 'color-domain', + content: (points) => shadcnTooltipContent(points), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = Math.min(250, contentWidth) + const chartHeight = chartWidth + const headerAction = null + const legend = null + const footer = ( + + ) + return ( +
+
+
+
+

Radial Chart - Stacked

+

January - June 2024

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ? ( +
{footer}
+ ) : null} +
+
+ ) +} +function TrendFooter({ note }: { note: string }) { + return ( + <> +
+ Trending up by 5.2% this month{' '} + +
+
{note}
+ + ) +} diff --git a/benchmarks/conformance/cases/189-shadcn-radial-stacked/styles.css b/benchmarks/conformance/cases/189-shadcn-radial-stacked/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/189-shadcn-radial-stacked/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/189-shadcn-radial-stacked/tanstack.ts b/benchmarks/conformance/cases/189-shadcn-radial-stacked/tanstack.ts index 31981c9a..433666b5 100644 --- a/benchmarks/conformance/cases/189-shadcn-radial-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/189-shadcn-radial-stacked/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-radial-stacked') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Radial Chart - Stacked implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/19-moving-average-line/example.tsx b/benchmarks/conformance/cases/19-moving-average-line/example.tsx new file mode 100644 index 00000000..8ca5c3c0 --- /dev/null +++ b/benchmarks/conformance/cases/19-moving-average-line/example.tsx @@ -0,0 +1,61 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, lineY, ruleY, rollingWindow } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { sfTemperatures } from '@charts-poc/demo-data/sf-temperatures' + +const windowSize = 14 + +export const definition = () => { + const completeRows = rollingWindow(sfTemperatures, { + size: windowSize, + partial: false, + outputs: { + high: { value: 'high', reduce: 'mean' }, + low: { value: 'low', reduce: 'mean' }, + }, + }) + const rows = completeRows + + return defineChart({ + marks: [ + lineY(rows, { + x: 'date', + y: 'low', + stroke: '#4e79a7', + strokeWidth: 2.25, + }), + lineY(rows, { + x: 'date', + y: 'high', + stroke: '#e15759', + strokeWidth: 2.25, + }), + ruleY([32], { + stroke: '#64748b', + strokeDasharray: '4 4', + }), + ], + x: { scale: scaleUtc, axis: { label: 'Date' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fourteen-day average temperature (°F)' }, + }, + }) +} +export const exampleAriaLabel = + 'Fourteen-day average high and low temperature in San Francisco' + +export const createExampleChart = () => + defineChart(definition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts b/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts index 480f55bb..352e831c 100644 --- a/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts +++ b/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts @@ -1,56 +1,11 @@ -import { defineChart, lineY, ruleY, rollingWindow } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { sfTemperatures } from '@charts-poc/demo-data/sf-temperatures' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const windowSize = 14 +export * from './example' -const definition = (input: ConformanceInput) => { - const completeRows = rollingWindow(sfTemperatures, { - size: windowSize, - partial: false, - outputs: { - high: { value: 'high', reduce: 'mean' }, - low: { value: 'low', reduce: 'mean' }, - }, - }) - const rows = samplePreviewData(completeRows, input, 80, [ - (row) => row.date.getTime(), - (row) => row.low, - (row) => row.high, - ]) - - return defineChart({ - marks: [ - lineY(rows, { - x: 'date', - y: 'low', - stroke: '#4e79a7', - strokeWidth: 2.25, - }), - lineY(rows, { - x: 'date', - y: 'high', - stroke: '#e15759', - strokeWidth: 2.25, - }), - ruleY([32], { - stroke: '#64748b', - strokeDasharray: '4 4', - }), - ], - x: { scale: scaleUtc, axis: { label: 'Date' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fourteen-day average temperature (°F)' }, - }, - }) -} - -export const mount = tanstackMount( - definition, - 'Fourteen-day average high and low temperature in San Francisco', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx b/benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx new file mode 100644 index 00000000..9fef5f69 --- /dev/null +++ b/benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Default

+

Default tooltip with ChartTooltipContent.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="default" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/190-shadcn-tooltip-default/styles.css b/benchmarks/conformance/cases/190-shadcn-tooltip-default/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/190-shadcn-tooltip-default/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/190-shadcn-tooltip-default/tanstack.ts b/benchmarks/conformance/cases/190-shadcn-tooltip-default/tanstack.ts index 1c0f2f30..24509d5e 100644 --- a/benchmarks/conformance/cases/190-shadcn-tooltip-default/tanstack.ts +++ b/benchmarks/conformance/cases/190-shadcn-tooltip-default/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-default') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Default implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx new file mode 100644 index 00000000..be8179c2 --- /dev/null +++ b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Formatter

+

Tooltip with custom formatter.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="formatter" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/styles.css b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/tanstack.ts b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/tanstack.ts index accb2ef4..47fef1fc 100644 --- a/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/tanstack.ts +++ b/benchmarks/conformance/cases/191-shadcn-tooltip-formatter/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-formatter') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Formatter implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx new file mode 100644 index 00000000..ac54cd21 --- /dev/null +++ b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx @@ -0,0 +1,280 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 - 27 + const headerAction = null + const legend = + const footer = null + return ( +
+
+
+
+

Tooltip - Icons

+

Tooltip with icons.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="icons" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} +function Legend() { + return ( + <> + {['desktop', 'mobile'].map((label, index) => ( + + + + + {titleCase(label)} + + ))} + + ) +} diff --git a/benchmarks/conformance/cases/192-shadcn-tooltip-icons/styles.css b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/192-shadcn-tooltip-icons/tanstack.ts b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/tanstack.ts index 1e5c0c36..13ccc89a 100644 --- a/benchmarks/conformance/cases/192-shadcn-tooltip-icons/tanstack.ts +++ b/benchmarks/conformance/cases/192-shadcn-tooltip-icons/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-icons') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Icons implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx new file mode 100644 index 00000000..d0c63aae --- /dev/null +++ b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Line Indicator

+

Tooltip with line indicator.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="indicator-line" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/styles.css b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/tanstack.ts b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/tanstack.ts index ffb2d6fc..5a0103bd 100644 --- a/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/tanstack.ts +++ b/benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-indicator-line') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Line Indicator implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx new file mode 100644 index 00000000..cbfdf1d9 --- /dev/null +++ b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - No Indicator

+

Tooltip with no indicator.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="indicator-none" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/styles.css b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/tanstack.ts b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/tanstack.ts index 57e36efa..0220bb10 100644 --- a/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/tanstack.ts +++ b/benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-indicator-none') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - No Indicator implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx new file mode 100644 index 00000000..249fbaf9 --- /dev/null +++ b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Custom label

+

Tooltip with custom label from chartConfig.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="label-custom" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/styles.css b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/tanstack.ts b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/tanstack.ts index 67907ecf..c1f35651 100644 --- a/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/tanstack.ts +++ b/benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-label-custom') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Custom label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx new file mode 100644 index 00000000..17cbc6c8 --- /dev/null +++ b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - Label Formatter

+

Tooltip with label formatter.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="label-formatter" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/styles.css b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/tanstack.ts b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/tanstack.ts index acfbfdc5..bf3510bf 100644 --- a/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/tanstack.ts +++ b/benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-label-formatter') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - Label Formatter implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx new file mode 100644 index 00000000..31df34fe --- /dev/null +++ b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx @@ -0,0 +1,260 @@ +import { useMemo, useRef } from 'react' +import { + barY, + defineChart, + stack, + type ChartPoint, + type ChartValue, + type DomChartDefinition, +} from '@tanstack/charts' +import { RendererChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { motion } from '@tanstack/charts/motion' +import { scaleBand, scaleLinear } from 'd3-scale' +import { + shadcnActivities, + shadcnColors, + type ShadcnActivityDatum, +} from '@charts-poc/demo-data/shadcn' +import './styles.css' +const activityNames = ['running', 'swimming'] as const +function createDefinition() { + return defineChart({ + marks: [ + barY(shadcnActivities, { + id: 'activity-bars', + x: 'date', + y: 'value', + z: 'activity', + color: 'activity', + key: (row) => `${row.date}:${row.activity}`, + layout: stack({ order: activityNames }), + radius: 4, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.1), + axis: { + line: false, + ticks: { size: 0, padding: 10, format: formatWeekday }, + }, + }, + y: { scale: scaleLinear().domain([0, 1000]), axis: false }, + color: { domain: activityNames, range: shadcnColors.slice(0, 2) }, + margin: { top: 0, right: 7, bottom: 32, left: 7 }, + theme: shadcnTheme(), + }) +} +function formatWeekday(value: string) { + return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format( + new Date(value), + ) +} +function shadcnTheme() { + return { + foreground: 'var(--muted-foreground, var(--muted))', + grid: 'var(--border)', + background: 'transparent', + } +} +function titleCase(value: string) { + return value.charAt(0).toUpperCase() + value.slice(1) +} +function ShadcnTooltipBody({ + variant, + points, +}: { + variant: string + points: readonly ChartPoint[] +}) { + const ordered = [...points].sort( + (left, right) => + activityNames.indexOf(left.datum.activity) - + activityNames.indexOf(right.datum.activity), + ) + const noLabel = + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' || + variant === 'advanced' + const noIndicator = + variant === 'indicator-none' || + variant === 'label-none' || + variant === 'formatter' || + variant === 'icons' + const lineIndicator = variant === 'indicator-line' + const formatted = variant === 'formatter' || variant === 'advanced' + const label = + variant === 'label-formatter' + ? 'July 15, 2024' + : variant === 'label-custom' + ? 'Activities' + : '2024-07-16' + return ( +
+ {noLabel ? null : {label}} + {ordered.map((point, index) => ( +
+ {variant === 'icons' ? ( + + ) : noIndicator ? null : ( + + )} + {titleCase(point.datum.activity)} + + {point.datum.value} + {formatted ? kcal : null} + +
+ ))} + {variant === 'advanced' ? ( +
+ Total + + {ordered.reduce((total, point) => total + point.datum.value, 0)} + kcal + +
+ ) : null} +
+ ) +} +function ShadcnActivityIcon({ activity }: { activity: string }) { + return ( + + {activity === 'running' ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} +export function createExampleChart() { + return defineChart(createDefinition(), { + svgAnimation: false, + focus: 'group-x', + keyboard: true, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: (_points, context) => ({ + x: context.surface.width * 0.271, + y: context.surface.height * 0.554, + }), + placement: 'bottom-right', + offset: 0, + sort: 'color-domain', + content: () => ({ rows: [] }), + }, + }) +} +export const definition = createExampleChart() +type ExampleDefinition = ReturnType +type ExampleDatum = + ExampleDefinition extends DomChartDefinition< + infer TDatum, + infer _TXValue, + infer _TYValue + > + ? TDatum + : never +type ExampleXValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer TXValue extends ChartValue, + infer _TYValue + > + ? TXValue + : never +type ExampleYValue = + ExampleDefinition extends DomChartDefinition< + infer _TDatum, + infer _TXValue, + infer TYValue extends ChartValue + > + ? TYValue + : never +export interface ExampleProps { + width?: number + height?: number +} +export default function Example({ width = 640, height = 600 }: ExampleProps) { + const seededTooltip = useRef(false) + const chartDefinition = definition + const renderer = useMemo( + () => + motion({ + initial: 'always', + transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 }, + }), + [], + ) + const contentWidth = Math.max(1, width - 50) + const chartWidth = contentWidth + const chartHeight = (contentWidth * 9) / 16 + const headerAction = null + const legend = null + const footer = null + return ( +
+
+
+
+

Tooltip - No Label

+

Tooltip with no label.

+
+ {headerAction ? ( +
{headerAction}
+ ) : null} +
+
+
+ { + if (seededTooltip.current) return + const point = scene.points.find( + (candidate) => + (candidate.datum as ShadcnActivityDatum).date === + '2024-07-16', + ) + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { + source: 'programmatic', + }) + }} + renderTooltipBody={({ points }) => ( + []} + variant="label-none" + /> + )} + /> +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/styles.css b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/styles.css new file mode 100644 index 00000000..7d46886a --- /dev/null +++ b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/styles.css @@ -0,0 +1,461 @@ +.sc-example { + --background: oklch(1 0 0); + --foreground: oklch(0 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --border: oklch(0.922 0 0); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + color: var(--foreground); + background: var(--background); + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + text-rendering: geometricPrecision; +} +:root[data-theme='dark'] .sc-example { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --border: oklch(1 0 0 / 10%); + --chart-1: oklch(0.809 0.105 251.813); + --chart-2: oklch(0.623 0.214 259.815); + --chart-3: oklch(0.546 0.245 262.881); + --chart-4: oklch(0.488 0.243 264.376); + --chart-5: oklch(0.424 0.199 265.638); +} +.sc-example, +.sc-example * { + box-sizing: border-box; +} +.sc-card { + display: flex; + flex-direction: column; + gap: 24px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--card); + color: var(--card-foreground); + padding: 24px 0; +} +.sc-card-header { + display: grid; + gap: 8px; + padding: 0 24px; +} +.sc-card-heading { + display: grid; + gap: 8px; +} +.sc-card-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.01em; +} +.sc-card-header p { + margin: 0; + color: var(--muted-foreground); + font-size: 14px; + line-height: 20px; +} +.sc-card-content { + display: flex; + min-height: 0; + flex-direction: column; + padding: 0 24px; +} +.sc-chart { + position: relative; + flex: none; +} +.sc-chart > * { + display: block; +} +.sc-chart-clip .ts-chart { + overflow: hidden !important; +} +.sc-card-footer { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: 0 24px; + font-size: 14px; + line-height: 1; +} +.sc-chart-footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 12px; + color: var(--muted-foreground); + font-size: 12px; +} +.sc-legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.sc-legend-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-legend-icon { + width: 12px; + height: 12px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-pie-legend { + display: flex; + width: 260px; + flex-wrap: wrap; + justify-content: center; + gap: 10px 16px; +} +.sc-pie-legend .sc-legend-item { + flex: 0 0 70px; + justify-content: center; +} +.sc-centered { + justify-content: center; + align-items: center; + text-align: center; +} +.sc-trend { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} +.sc-trend svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-footer-note { + color: var(--muted-foreground); +} +.sc-example .ts-chart { + color: var(--muted-foreground); +} +.sc-example .recharts-surface { + overflow: visible; +} +.sc-example .ts-chart__grid line, +.sc-example .ts-chart__polar-grid path, +.sc-example .ts-chart__polar-grid line { + stroke: var(--border); +} +.sc-example .ts-chart__axes text, +.sc-example .ts-chart__polar-grid text { + fill: var(--muted-foreground); + font-size: 12px; +} +.sc-example .ts-chart-tooltip { + min-width: 128px; + padding: 7px 10px !important; + border: 1px solid var(--border) !important; + border-radius: 8px !important; + background: var(--card) !important; + color: var(--card-foreground) !important; + box-shadow: 0 2px 6px rgb(0 0 0 / 8%); + font-size: 12px; +} +.sc-advanced-tooltip { + display: grid; + width: 158px; + gap: 6px; +} +.sc-advanced-row { + display: grid; + grid-template-columns: 10px 1fr auto; + align-items: center; + gap: 6px; +} +.sc-advanced-swatch { + width: 10px; + height: 10px; + border-radius: 2px; +} +.sc-advanced-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-advanced-unit { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-advanced-total { + display: flex; + align-items: center; + margin-top: 2px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-advanced-total .sc-advanced-value { + margin-left: auto; +} +.sc-shadcn-tooltip { + display: grid; + gap: 5px; +} +.sc-tooltip-label { + color: var(--foreground); + font-weight: 500; +} +.sc-shadcn-tooltip-row { + display: grid; + grid-template-columns: 9px minmax(0, 1fr) auto; + align-items: center; + gap: 7px; + color: var(--muted-foreground); +} +.sc-tooltip-no-indicator .sc-shadcn-tooltip-row { + grid-template-columns: minmax(0, 1fr) auto; +} +.sc-tooltip-dot { + width: 8px; + height: 8px; + border-radius: 2px; +} +.sc-tooltip-line { + width: 3px; + height: 16px; + border-radius: 2px; +} +.sc-tooltip-icon { + width: 11px; + height: 11px; + fill: none; + stroke: var(--muted-foreground); + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} +.sc-tooltip-value { + display: flex; + align-items: baseline; + gap: 2px; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 500; + font-variant-numeric: tabular-nums; +} +.sc-tooltip-value span { + color: var(--muted-foreground); + font-family: inherit; + font-weight: 400; +} +.sc-tooltip-total { + display: flex; + justify-content: space-between; + margin-top: 1px; + padding-top: 7px; + border-top: 1px solid var(--border); + color: var(--foreground); + font-weight: 500; +} +.sc-interactive-area { + gap: 0; + padding-top: 0; +} +.sc-interactive-area .sc-card-header { + display: flex; + align-items: center; + gap: 16px; + padding-top: 20px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border); +} +.sc-interactive-area .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-area .sc-card-content { + padding-top: 24px; +} +.sc-interactive-area .sc-chart-footer { + margin-top: 8px; +} +.sc-interactive-bar { + gap: 0; + padding: 0 0 30px; +} +.sc-interactive-bar .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-bar .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-bar .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-bar .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-line { + gap: 0; + padding: 0; +} +.sc-interactive-line .sc-card-header { + display: flex; + min-height: 99px; + align-items: stretch; + gap: 0; + padding: 0; + border-bottom: 1px solid var(--border); +} +.sc-interactive-line .sc-card-heading { + flex: 1; + align-content: center; + gap: 4px; + padding: 16px 24px; +} +.sc-interactive-line .sc-card-action { + display: flex; + margin: 0; +} +.sc-interactive-line .sc-card-content { + padding: 43px 24px 0; +} +.sc-interactive-pie .sc-card-header { + display: flex; + align-items: flex-start; + gap: 16px; + padding-bottom: 0; +} +.sc-interactive-pie .sc-card-heading { + flex: 1; + gap: 4px; +} +.sc-interactive-pie .sc-card-content { + padding-bottom: 44px; +} +.sc-interactive-pie .sc-chart { + transform: translateY(55px); +} +.sc-interactive-pie .sc-card-action { + transform: translateY(48px); +} +.sc-card-action { + margin-left: auto; +} +.sc-select-display { + position: relative; + display: flex; + min-width: 130px; + height: 36px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--background); + color: var(--foreground); + font-size: 14px; + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); +} +.sc-select-display:focus-within { + outline: 2px solid var(--foreground); + outline-offset: 2px; +} +.sc-select-display select { + min-width: 0; + flex: 1; + appearance: none; + border: 0; + outline: 0; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; +} +.sc-select-display svg { + width: 14px; + height: 14px; + flex: none; + color: var(--muted-foreground); + pointer-events: none; +} +.sc-select-swatch { + width: 12px; + height: 12px; + flex: none; + border-radius: 3px; +} +.sc-bar-metric { + display: grid; + width: 168px; + align-content: center; + gap: 6px; + padding: 16px 32px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} +.sc-bar-metric[data-active='true'] { + background: var(--muted); +} +.sc-bar-metric:focus-visible { + outline: 2px solid var(--foreground); + outline-offset: -3px; +} +.sc-bar-metric span { + color: var(--muted-foreground); + font-size: 12px; +} +.sc-bar-metric strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; +} diff --git a/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/tanstack.ts b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/tanstack.ts index dcfea0be..a1153bc9 100644 --- a/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/tanstack.ts +++ b/benchmarks/conformance/cases/197-shadcn-tooltip-label-none/tanstack.ts @@ -1,7 +1,18 @@ -import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack' +import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' -const example = createShadcnTanStackExample('chart-tooltip-label-none') +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) -export const shadcnDefinition = example.definition -export const catalogCase = example.catalogCase -export const mount = example.mount +export * from './example' +export const shadcnDefinition = definition + +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + 'Tooltip - No Label implemented with TanStack Charts', + { guides: true, margin: true }, +) diff --git a/benchmarks/conformance/cases/20-normalized-stacked-area/example.tsx b/benchmarks/conformance/cases/20-normalized-stacked-area/example.tsx new file mode 100644 index 00000000..08bfd7e4 --- /dev/null +++ b/benchmarks/conformance/cases/20-normalized-stacked-area/example.tsx @@ -0,0 +1,72 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaY, colorLegend, defineChart, ruleY, stack } from '@tanstack/charts' +import { format } from 'd3-format' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' + +const percent = format('.0%') +const colors = [ + '#4e79a7', + '#f28e2c', + '#e15759', + '#76b7b2', + '#59a14f', + '#edc949', + '#af7aa1', + '#ff9da7', + '#9c755f', + '#bab0ab', +] + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + areaY(industries, { + x: 'date', + y: 'unemployed', + color: 'industry', + fillOpacity: 0.82, + layout: stack({ offset: 'normalize' }), + }), + ruleY([0]), + ], + x: { scale: scaleUtc, axis: { label: 'Month' } }, + y: { + scale: scaleLinear().domain([0, 1]), + grid: true, + axis: { ticks: { format: percent }, label: 'Share of unemployment' }, + }, + color: { + range: colors, + ...(input.preview === true + ? {} + : { legend: colorLegend({ label: 'Industry' }) }), + }, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Industry share of unemployment' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts b/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts index 46920dd7..6c029e65 100644 --- a/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts +++ b/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts @@ -1,56 +1,8 @@ -import { areaY, colorLegend, defineChart, ruleY, stack } from '@tanstack/charts' -import { format } from 'd3-format' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewSeries } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const percent = format('.0%') -const colors = [ - '#4e79a7', - '#f28e2c', - '#e15759', - '#76b7b2', - '#59a14f', - '#edc949', - '#af7aa1', - '#ff9da7', - '#9c755f', - '#bab0ab', -] +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - areaY( - samplePreviewSeries(industries, input, 32, (row) => row.industry), - { - x: 'date', - y: 'unemployed', - color: 'industry', - fillOpacity: 0.82, - layout: stack({ offset: 'normalize' }), - }, - ), - ruleY([0]), - ], - x: { scale: scaleUtc, axis: { label: 'Month' } }, - y: { - scale: scaleLinear().domain([0, 1]), - grid: true, - axis: { ticks: { format: percent }, label: 'Share of unemployment' }, - }, - color: { - range: colors, - ...(input.preview === true - ? {} - : { legend: colorLegend({ label: 'Industry' }) }), - }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'Industry share of unemployment', - true, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/21-streamgraph/example.tsx b/benchmarks/conformance/cases/21-streamgraph/example.tsx new file mode 100644 index 00000000..18c27ac6 --- /dev/null +++ b/benchmarks/conformance/cases/21-streamgraph/example.tsx @@ -0,0 +1,71 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaY, colorLegend, defineChart, stack } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' +import type { IndustriesRow } from '@charts-poc/demo-data/industries' + +const colors = [ + '#4e79a7', + '#f28e2c', + '#e15759', + '#76b7b2', + '#59a14f', + '#edc949', + '#af7aa1', + '#ff9da7', + '#9c755f', + '#bab0ab', +] + +function streamgraphChart(rows: readonly IndustriesRow[], showLegend: boolean) { + return defineChart({ + marks: [ + areaY(rows, { + x: 'date', + y: 'unemployed', + z: 'industry', + color: 'industry', + layout: stack({ offset: 'wiggle', order: 'inside-out' }), + fillOpacity: 0.85, + }), + ], + x: { scale: scaleUtc, axis: { label: 'Month' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Unemployed (thousands)' }, + }, + color: { + range: colors, + ...(showLegend ? { legend: colorLegend({ label: 'Industry' }) } : {}), + }, + }) +} + +export const streamgraphDefinition = () => streamgraphChart(industries, true) + +export const exampleAriaLabel = 'Unemployment by industry as a streamgraph' + +export const createExampleChart = () => + defineChart(streamgraphDefinition(), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + })} · ${datum.unemployed.toLocaleString('en-US')} thousand unemployed`, + }, + }, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/21-streamgraph/tanstack.test.ts b/benchmarks/conformance/cases/21-streamgraph/tanstack.test.ts index d6abbb62..64a53cbf 100644 --- a/benchmarks/conformance/cases/21-streamgraph/tanstack.test.ts +++ b/benchmarks/conformance/cases/21-streamgraph/tanstack.test.ts @@ -53,7 +53,7 @@ describe('definition-owned streamgraph stack', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/21-streamgraph/tanstack.ts', + 'benchmarks/conformance/cases/21-streamgraph/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/21-streamgraph/tanstack.ts b/benchmarks/conformance/cases/21-streamgraph/tanstack.ts index bf31cca8..352e831c 100644 --- a/benchmarks/conformance/cases/21-streamgraph/tanstack.ts +++ b/benchmarks/conformance/cases/21-streamgraph/tanstack.ts @@ -1,72 +1,11 @@ -import { areaY, colorLegend, defineChart, stack } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewSeries } from '../../shared/preview' -import type { IndustriesRow } from '@charts-poc/demo-data/industries' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#4e79a7', - '#f28e2c', - '#e15759', - '#76b7b2', - '#59a14f', - '#edc949', - '#af7aa1', - '#ff9da7', - '#9c755f', - '#bab0ab', -] +export * from './example' -function streamgraphChart(rows: readonly IndustriesRow[], showLegend: boolean) { - return defineChart({ - marks: [ - areaY(rows, { - x: 'date', - y: 'unemployed', - z: 'industry', - color: 'industry', - layout: stack({ offset: 'wiggle', order: 'inside-out' }), - fillOpacity: 0.85, - }), - ], - x: { scale: scaleUtc, axis: { label: 'Month' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Unemployed (thousands)' }, - }, - color: { - range: colors, - ...(showLegend ? { legend: colorLegend({ label: 'Industry' }) } : {}), - }, - }) -} - -export const streamgraphDefinition = () => streamgraphChart(industries, true) - -const catalogStreamgraphDefinition = (input: ConformanceInput) => - streamgraphChart( - samplePreviewSeries(industries, input, 24, (row) => row.industry), - input.preview !== true, - ) - -export const mount = tanstackMount( - streamgraphDefinition, - 'Unemployment by industry as a streamgraph', - { - format: ({ datum }) => - `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { - month: 'short', - year: 'numeric', - timeZone: 'UTC', - })} · ${datum.unemployed.toLocaleString('en-US')} thousand unemployed`, - }, +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const catalogCase = tanstackCase( - catalogStreamgraphDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/22-bollinger-band/example.tsx b/benchmarks/conformance/cases/22-bollinger-band/example.tsx new file mode 100644 index 00000000..7481a64b --- /dev/null +++ b/benchmarks/conformance/cases/22-bollinger-band/example.tsx @@ -0,0 +1,89 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + areaY, + defineChart, + deviation, + lineY, + rollingWindow, +} from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { aapl } from '@charts-poc/demo-data/aapl' +import { selectBollingerData } from './selection' + +const windowSize = 20 +const deviationMultiplier = 2 + +function bollingerRows(input: ExampleOptions) { + const rows = rollingWindow(selectBollingerData(aapl, input.revision), { + size: windowSize, + orderBy: 'Date', + anchor: 'end', + partial: false, + outputs: { + meanClose: { value: 'Close', reduce: 'mean' }, + closeDeviation: { value: 'Close', reduce: deviation }, + }, + }) + return rows +} + +function bollingerChart( + rows: readonly ReturnType[number][], +) { + return defineChart({ + marks: [ + areaY(rows, { + id: 'bollinger-band', + x: 'Date', + y1: (row) => row.meanClose - row.closeDeviation * deviationMultiplier, + y2: (row) => row.meanClose + row.closeDeviation * deviationMultiplier, + fill: '#7c3aed', + fillOpacity: 0.18, + }), + lineY(rows, { + id: 'bollinger-mean', + x: 'Date', + y: 'meanClose', + stroke: '#7c3aed', + strokeWidth: 2.25, + }), + ], + x: { scale: scaleUtc, axis: { label: 'Date' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, + }) +} + +export const bollingerDefinition = (input: ExampleOptions) => + bollingerChart(bollingerRows(input)) + +export const catalogBollingerDefinition = (input: ExampleOptions) => { + const rows = bollingerRows(input) + return bollingerChart(rows) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Twenty-day Apple Bollinger band' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(catalogBollingerDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/22-bollinger-band/tanstack.test.ts b/benchmarks/conformance/cases/22-bollinger-band/tanstack.test.ts index 9692d175..4b018627 100644 --- a/benchmarks/conformance/cases/22-bollinger-band/tanstack.test.ts +++ b/benchmarks/conformance/cases/22-bollinger-band/tanstack.test.ts @@ -67,7 +67,7 @@ describe('definition-owned Bollinger band', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/22-bollinger-band/tanstack.ts', + 'benchmarks/conformance/cases/22-bollinger-band/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts b/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts index ad25cec8..6c029e65 100644 --- a/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts +++ b/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts @@ -1,77 +1,8 @@ -import { - areaY, - defineChart, - deviation, - lineY, - rollingWindow, -} from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { aapl } from '@charts-poc/demo-data/aapl' -import { tanstackCase } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' -import { selectBollingerData } from './selection' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const windowSize = 20 -const deviationMultiplier = 2 +export * from './example' -function bollingerRows(input: ConformanceInput) { - const rows = rollingWindow(selectBollingerData(aapl, input.revision), { - size: windowSize, - orderBy: 'Date', - anchor: 'end', - partial: false, - outputs: { - meanClose: { value: 'Close', reduce: 'mean' }, - closeDeviation: { value: 'Close', reduce: deviation }, - }, - }) - return rows -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -function bollingerChart( - rows: readonly ReturnType[number][], -) { - return defineChart({ - marks: [ - areaY(rows, { - id: 'bollinger-band', - x: 'Date', - y1: (row) => row.meanClose - row.closeDeviation * deviationMultiplier, - y2: (row) => row.meanClose + row.closeDeviation * deviationMultiplier, - fill: '#7c3aed', - fillOpacity: 0.18, - }), - lineY(rows, { - id: 'bollinger-mean', - x: 'Date', - y: 'meanClose', - stroke: '#7c3aed', - strokeWidth: 2.25, - }), - ], - x: { scale: scaleUtc, axis: { label: 'Date' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, - }) -} - -export const bollingerDefinition = (input: ConformanceInput) => - bollingerChart(bollingerRows(input)) - -const catalogBollingerDefinition = (input: ConformanceInput) => { - const rows = bollingerRows(input) - return bollingerChart( - samplePreviewData(rows, input, 80, [ - (row) => row.Date.getTime(), - (row) => row.meanClose - row.closeDeviation * deviationMultiplier, - (row) => row.meanClose + row.closeDeviation * deviationMultiplier, - ]), - ) -} - -export const catalogCase = tanstackCase( - catalogBollingerDefinition, - 'Twenty-day Apple Bollinger band', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/24-quantitative-binned-heatmap/example.tsx b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/example.tsx new file mode 100644 index 00000000..663f2082 --- /dev/null +++ b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/example.tsx @@ -0,0 +1,89 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { binXY, colorGradientLegend, defineChart, rect } from '@tanstack/charts' +import { scaleLinear, scaleSequential } from 'd3-scale' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +type PenguinBill = PenguinsRow & { + readonly culmen_length_mm: number + readonly culmen_depth_mm: number +} + +const xBoundaries = [30, 34, 38, 42, 46, 50, 54, 58, 62] +const yBoundaries = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] +export const definition = (input: ExampleOptions) => { + const rows = penguins + .filter((row): row is PenguinBill => { + return row.culmen_length_mm !== null && row.culmen_depth_mm !== null + }) + .slice(input.revision * 8, input.revision * 8 + 320) + + const cells = binXY(rows, { + x: 'culmen_length_mm', + y: 'culmen_depth_mm', + xThresholds: xBoundaries, + yThresholds: yBoundaries, + outputs: { count: { reduce: 'count' } }, + }).filter((cell) => cell.count > 0) + + return defineChart({ + marks: [ + rect(cells, { + x1: 'x1', + x2: 'x2', + y1: 'y1', + y2: 'y2', + color: 'count', + inset: 0.75, + }), + ], + x: { + scale: scaleLinear().domain([30, 62]), + grid: true, + axis: { label: 'Bill length (mm)' }, + }, + y: { + scale: scaleLinear().domain([12, 23]), + grid: true, + axis: { label: 'Bill depth (mm)' }, + }, + color: { + scale: scaleSequential, + range: ['#eff6ff', '#1d4ed8'], + legend: colorGradientLegend({ label: 'Count', steps: 5 }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Quantitative two-dimensional binned heatmap' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `Bill length: ${point.datum.x1}–${point.datum.x2} mm · Bill depth: ${point.datum.y1}–${point.datum.y2} mm · Penguins: ${point.datum.count}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts index 90d5b695..6c029e65 100644 --- a/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts +++ b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts @@ -1,66 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { binXY, colorGradientLegend, defineChart, rect } from '@tanstack/charts' -import { scaleLinear, scaleSequential } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type PenguinBill = PenguinsRow & { - readonly culmen_length_mm: number - readonly culmen_depth_mm: number -} +export * from './example' -const xBoundaries = [30, 34, 38, 42, 46, 50, 54, 58, 62] -const yBoundaries = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] -const definition = (input: ConformanceInput) => { - const rows = penguins - .filter((row): row is PenguinBill => { - return row.culmen_length_mm !== null && row.culmen_depth_mm !== null - }) - .slice(input.revision * 8, input.revision * 8 + 320) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - const cells = binXY(rows, { - x: 'culmen_length_mm', - y: 'culmen_depth_mm', - xThresholds: xBoundaries, - yThresholds: yBoundaries, - outputs: { count: { reduce: 'count' } }, - }).filter((cell) => cell.count > 0) - - return defineChart({ - marks: [ - rect(cells, { - x1: 'x1', - x2: 'x2', - y1: 'y1', - y2: 'y2', - color: 'count', - inset: 0.75, - }), - ], - x: { - scale: scaleLinear().domain([30, 62]), - grid: true, - axis: { label: 'Bill length (mm)' }, - }, - y: { - scale: scaleLinear().domain([12, 23]), - grid: true, - axis: { label: 'Bill depth (mm)' }, - }, - color: { - scale: scaleSequential, - range: ['#eff6ff', '#1d4ed8'], - legend: colorGradientLegend({ label: 'Count', steps: 5 }), - }, - }) -} - -export const mount = tanstackMount( - definition, - 'Quantitative two-dimensional binned heatmap', - { - format: (point) => - `Bill length: ${point.datum.x1}–${point.datum.x2} mm · Bill depth: ${point.datum.y1}–${point.datum.y2} mm · Penguins: ${point.datum.count}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/25-calendar-heatmap/example.tsx b/benchmarks/conformance/cases/25-calendar-heatmap/example.tsx new file mode 100644 index 00000000..583c339f --- /dev/null +++ b/benchmarks/conformance/cases/25-calendar-heatmap/example.tsx @@ -0,0 +1,70 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cell, colorGradientLegend, defineChart } from '@tanstack/charts' +import { weather } from '@charts-poc/demo-data/weather' +import { scaleBand, scaleSequential } from 'd3-scale' +import { utcSunday } from 'd3-time' +import { selectCalendarData } from './selection' + +const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] + +export const definition = (input: ExampleOptions) => { + const rows = selectCalendarData(weather, input.revision) + const calendarStart = rows[0]?.date + if (calendarStart === undefined) { + throw new TypeError('The weather calendar selection is empty') + } + + return defineChart({ + marks: [ + cell(rows, { + x: (row) => utcSunday.count(calendarStart, row.date), + y: (row) => weekdays[row.date.getUTCDay()], + color: 'precipitation', + inset: 1, + radius: 2, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.06).paddingOuter(0.03), + axis: { ticks: { format: (value) => `W${value + 1}` }, label: 'Week' }, + }, + y: { + scale: scaleBand() + .domain(weekdays) + .paddingInner(0.06) + .paddingOuter(0.03), + }, + color: { + scale: scaleSequential, + range: ['#ecfdf5', '#047857'], + legend: colorGradientLegend({ label: 'Precipitation (mm)', steps: 6 }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Fourteen-week Seattle precipitation heatmap' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts b/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts index 37c0fb58..6c029e65 100644 --- a/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts +++ b/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts @@ -1,49 +1,8 @@ -import { cell, colorGradientLegend, defineChart } from '@tanstack/charts' -import { weather } from '@charts-poc/demo-data/weather' -import { scaleBand, scaleSequential } from 'd3-scale' -import { utcSunday } from 'd3-time' -import { selectCalendarData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = selectCalendarData(weather, input.revision) - const calendarStart = rows[0]?.date - if (calendarStart === undefined) { - throw new TypeError('The weather calendar selection is empty') - } +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - cell(rows, { - x: (row) => utcSunday.count(calendarStart, row.date), - y: (row) => weekdays[row.date.getUTCDay()], - color: 'precipitation', - inset: 1, - radius: 2, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.06).paddingOuter(0.03), - axis: { ticks: { format: (value) => `W${value + 1}` }, label: 'Week' }, - }, - y: { - scale: scaleBand() - .domain(weekdays) - .paddingInner(0.06) - .paddingOuter(0.03), - }, - color: { - scale: scaleSequential, - range: ['#ecfdf5', '#047857'], - legend: colorGradientLegend({ label: 'Precipitation (mm)', steps: 6 }), - }, - }) -} - -export const mount = tanstackMount( - definition, - 'Fourteen-week Seattle precipitation heatmap', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/26-diverging-likert/example.tsx b/benchmarks/conformance/cases/26-diverging-likert/example.tsx new file mode 100644 index 00000000..27c6b6d6 --- /dev/null +++ b/benchmarks/conformance/cases/26-diverging-likert/example.tsx @@ -0,0 +1,72 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + barX, + colorLegend, + defineChart, + groupBy, + ruleX, + stack, +} from '@tanstack/charts' +import { survey } from '@charts-poc/demo-data/survey' +import { scaleBand, scaleLinear } from 'd3-scale' +import { likertResponses, selectLikertSurvey } from './selection' + +const colors = ['#991b1b', '#ef4444', '#cbd5e1', '#60a5fa', '#1d4ed8'] + +const likertSurvey = selectLikertSurvey(survey) +export const likertCounts = groupBy(likertSurvey, { + by: { Question: 'Question', Response: 'Response' }, + outputs: { count: { reduce: 'count' } }, +}) + +export const likertDefinition = () => + defineChart({ + marks: [ + barX(likertCounts, { + id: 'likert-responses', + x: 'count', + y: 'Question', + z: 'Response', + color: 'Response', + layout: stack({ + order: likertResponses, + anchor: { series: 'Neutral', fraction: 0.5 }, + }), + key: (row) => `${row.Question}:${row.Response}`, + inset: 0.75, + }), + ruleX([0], { stroke: '#64748b' }), + ], + x: { + scale: scaleLinear, + grid: true, + axis: { + ticks: { format: (value) => `${Math.abs(value)}` }, + label: '← more disagree · Number of responses · more agree →', + }, + }, + y: { + scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.08), + }, + color: { + domain: likertResponses, + range: colors, + legend: colorLegend({ label: 'Response' }), + }, + }) + +export const exampleAriaLabel = 'Diverging Likert survey responses' + +export const createExampleChart = () => + defineChart(likertDefinition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/26-diverging-likert/tanstack.test.ts b/benchmarks/conformance/cases/26-diverging-likert/tanstack.test.ts index c741d130..fb0d60a9 100644 --- a/benchmarks/conformance/cases/26-diverging-likert/tanstack.test.ts +++ b/benchmarks/conformance/cases/26-diverging-likert/tanstack.test.ts @@ -80,7 +80,7 @@ describe('definition-owned anchored Likert stack', () => { process.cwd(), 'benchmarks/conformance/cases/26-diverging-likert', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') expect(source).toContain('groupBy(likertSurvey') expect(source).toContain("outputs: { count: { reduce: 'count' } }") diff --git a/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts b/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts index ee409bd7..352e831c 100644 --- a/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts +++ b/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts @@ -1,63 +1,11 @@ -import { - barX, - colorLegend, - defineChart, - groupBy, - ruleX, - stack, -} from '@tanstack/charts' -import { survey } from '@charts-poc/demo-data/survey' -import { scaleBand, scaleLinear } from 'd3-scale' -import { likertResponses, selectLikertSurvey } from './selection' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#991b1b', '#ef4444', '#cbd5e1', '#60a5fa', '#1d4ed8'] +export * from './example' -const likertSurvey = selectLikertSurvey(survey) -export const likertCounts = groupBy(likertSurvey, { - by: { Question: 'Question', Response: 'Response' }, - outputs: { count: { reduce: 'count' } }, -}) - -export const likertDefinition = () => - defineChart({ - marks: [ - barX(likertCounts, { - id: 'likert-responses', - x: 'count', - y: 'Question', - z: 'Response', - color: 'Response', - layout: stack({ - order: likertResponses, - anchor: { series: 'Neutral', fraction: 0.5 }, - }), - key: (row) => `${row.Question}:${row.Response}`, - inset: 0.75, - }), - ruleX([0], { stroke: '#64748b' }), - ], - x: { - scale: scaleLinear, - grid: true, - axis: { - ticks: { format: (value) => `${Math.abs(value)}` }, - label: '← more disagree · Number of responses · more agree →', - }, - }, - y: { - scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.08), - }, - color: { - domain: likertResponses, - range: colors, - legend: colorLegend({ label: 'Response' }), - }, - }) - -export const catalogCase = tanstackCase( - likertDefinition, - 'Diverging Likert survey responses', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/27-parallel-coordinates/example.tsx b/benchmarks/conformance/cases/27-parallel-coordinates/example.tsx new file mode 100644 index 00000000..edf3ec18 --- /dev/null +++ b/benchmarks/conformance/cases/27-parallel-coordinates/example.tsx @@ -0,0 +1,89 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + colorLegend, + defineChart, + dot, + lineY, + normalize, + select, +} from '@tanstack/charts' +import { fold } from '@tanstack/charts/transform/fold' +import { decathlon } from '@charts-poc/demo-data/decathlon' +import { scaleBand, scaleLinear } from 'd3-scale' +import { decathlonEvents, timedEvents } from './selection' + +const colors = [ + '#2563eb', + '#ea580c', + '#059669', + '#7c3aed', + '#db2777', + '#0891b2', + '#ca8a04', +] + +export const foldedDecathlon = fold(decathlon, { + fields: decathlonEvents, + as: { key: 'event', value: 'result' }, +}) +export const normalizedDecathlon = normalize(foldedDecathlon, { + by: 'event', + value: (datum) => + timedEvents.has(datum.event) ? -datum.result : datum.result, + basis: 'extent', + as: 'relativePerformance', +}) +export const representativeProfiles = select(normalizedDecathlon, { + by: { Country: 'Country', event: 'event' }, + select: 'last', +}) + +export const parallelCoordinatesDefinition = () => + defineChart({ + marks: [ + lineY(representativeProfiles, { + id: 'country-lines', + x: 'event', + y: ({ relativePerformance }) => relativePerformance * 100, + color: 'Country', + key: ({ Country, event }) => `${Country}:${event}`, + strokeWidth: 1.75, + }), + dot(representativeProfiles, { + id: 'country-points', + x: 'event', + y: ({ relativePerformance }) => relativePerformance * 100, + color: 'Country', + key: ({ Country, event }) => `${Country}:${event}`, + r: 2.75, + }), + ], + x: { + scale: scaleBand().domain(decathlonEvents).padding(0.1), + }, + y: { + scale: scaleLinear().domain([0, 100]), + grid: true, + axis: { label: 'Relative performance within sample' }, + }, + color: { + range: colors, + legend: colorLegend({ label: 'Country' }), + }, + }) + +export const exampleAriaLabel = 'Parallel coordinates model comparison' + +export const createExampleChart = () => + defineChart(parallelCoordinatesDefinition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.test.ts b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.test.ts index 82f5a534..577d2812 100644 --- a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.test.ts +++ b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.test.ts @@ -79,7 +79,7 @@ describe('folded parallel coordinates', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts', + 'benchmarks/conformance/cases/27-parallel-coordinates/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts index 2e4c533f..352e831c 100644 --- a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts +++ b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts @@ -1,80 +1,11 @@ -import { - colorLegend, - defineChart, - dot, - lineY, - normalize, - select, -} from '@tanstack/charts' -import { fold } from '@tanstack/charts/transform/fold' -import { decathlon } from '@charts-poc/demo-data/decathlon' -import { scaleBand, scaleLinear } from 'd3-scale' -import { decathlonEvents, timedEvents } from './selection' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#2563eb', - '#ea580c', - '#059669', - '#7c3aed', - '#db2777', - '#0891b2', - '#ca8a04', -] +export * from './example' -export const foldedDecathlon = fold(decathlon, { - fields: decathlonEvents, - as: { key: 'event', value: 'result' }, -}) -export const normalizedDecathlon = normalize(foldedDecathlon, { - by: 'event', - value: (datum) => - timedEvents.has(datum.event) ? -datum.result : datum.result, - basis: 'extent', - as: 'relativePerformance', -}) -export const representativeProfiles = select(normalizedDecathlon, { - by: { Country: 'Country', event: 'event' }, - select: 'last', -}) - -export const parallelCoordinatesDefinition = () => - defineChart({ - marks: [ - lineY(representativeProfiles, { - id: 'country-lines', - x: 'event', - y: ({ relativePerformance }) => relativePerformance * 100, - color: 'Country', - key: ({ Country, event }) => `${Country}:${event}`, - strokeWidth: 1.75, - }), - dot(representativeProfiles, { - id: 'country-points', - x: 'event', - y: ({ relativePerformance }) => relativePerformance * 100, - color: 'Country', - key: ({ Country, event }) => `${Country}:${event}`, - r: 2.75, - }), - ], - x: { - scale: scaleBand().domain(decathlonEvents).padding(0.1), - }, - y: { - scale: scaleLinear().domain([0, 100]), - grid: true, - axis: { label: 'Relative performance within sample' }, - }, - color: { - range: colors, - legend: colorLegend({ label: 'Country' }), - }, - }) - -export const catalogCase = tanstackCase( - parallelCoordinatesDefinition, - 'Parallel coordinates model comparison', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/28-candlestick/example.tsx b/benchmarks/conformance/cases/28-candlestick/example.tsx new file mode 100644 index 00000000..bd5ca819 --- /dev/null +++ b/benchmarks/conformance/cases/28-candlestick/example.tsx @@ -0,0 +1,85 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, link } from '@tanstack/charts' +import { aapl } from '@charts-poc/demo-data/aapl' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { selectCandleData } from './selection' + +const candleDate = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', +}) +const price = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}) + +export const definition = (input: ExampleOptions) => { + const rows = selectCandleData(aapl, input.revision) + const gains = rows.filter((row) => row.Close >= row.Open) + const losses = rows.filter((row) => row.Close < row.Open) + return defineChart({ + marks: [ + link(rows, { + x1: 'Date', + y1: 'Low', + x2: 'Date', + y2: 'High', + stroke: '#64748b', + strokeWidth: 1, + }), + link(gains, { + x1: 'Date', + y1: 'Open', + x2: 'Date', + y2: 'Close', + stroke: '#10b981', + strokeWidth: 5, + }), + link(losses, { + x1: 'Date', + y1: 'Open', + x2: 'Date', + y2: 'Close', + stroke: '#ef4444', + strokeWidth: 5, + }), + ], + x: { scale: scaleUtc }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Price' } }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Apple daily candlestick chart' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `${candleDate.format(point.datum.Date)} · Open: ${price.format(point.datum.Open)} · High: ${price.format(point.datum.High)} · Low: ${price.format(point.datum.Low)} · Close: ${price.format(point.datum.Close)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/28-candlestick/tanstack.ts b/benchmarks/conformance/cases/28-candlestick/tanstack.ts index f1a1872f..6c029e65 100644 --- a/benchmarks/conformance/cases/28-candlestick/tanstack.ts +++ b/benchmarks/conformance/cases/28-candlestick/tanstack.ts @@ -1,64 +1,8 @@ -import { defineChart, link } from '@tanstack/charts' -import { aapl } from '@charts-poc/demo-data/aapl' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { selectCandleData } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const candleDate = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - timeZone: 'UTC', -}) -const price = new Intl.NumberFormat('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, -}) +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = selectCandleData(aapl, input.revision) - const gains = rows.filter((row) => row.Close >= row.Open) - const losses = rows.filter((row) => row.Close < row.Open) - return defineChart({ - marks: [ - link(rows, { - x1: 'Date', - y1: 'Low', - x2: 'Date', - y2: 'High', - stroke: '#64748b', - strokeWidth: 1, - }), - link(gains, { - x1: 'Date', - y1: 'Open', - x2: 'Date', - y2: 'Close', - stroke: '#10b981', - strokeWidth: 5, - }), - link(losses, { - x1: 'Date', - y1: 'Open', - x2: 'Date', - y2: 'Close', - stroke: '#ef4444', - strokeWidth: 5, - }), - ], - x: { scale: scaleUtc }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Price' } }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const catalogCase = tanstackCase( - definition, - 'Apple daily candlestick chart', - { - format: (point) => - `${candleDate.format(point.datum.Date)} · Open: ${price.format(point.datum.Open)} · High: ${price.format(point.datum.High)} · Low: ${price.format(point.datum.Low)} · Close: ${price.format(point.datum.Close)}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/29-waterfall/example.tsx b/benchmarks/conformance/cases/29-waterfall/example.tsx new file mode 100644 index 00000000..aa299583 --- /dev/null +++ b/benchmarks/conformance/cases/29-waterfall/example.tsx @@ -0,0 +1,108 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + barY, + colorLegend, + defineChart, + delta, + ruleY, + rollingWindow, +} from '@tanstack/charts' +import { waterfall } from '@tanstack/charts/transform/waterfall' +import type { WaterfallKind } from '@tanstack/charts/transform/waterfall' +import { scaleBand, scaleLinear } from 'd3-scale' +import { driving } from '@charts-poc/demo-data/driving' + +const kinds = [ + 'increase', + 'decrease', + 'total', +] satisfies readonly WaterfallKind[] +const colors = ['#10b981', '#ef4444', '#2563eb'] +const signedAmount = new Intl.NumberFormat('en-US', { + signDisplay: 'always', + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, +}) + +const observations = driving.filter((row) => row.year >= 2004) +const firstYear = observations[0]?.year +const lastYear = observations.at(-1)?.year +const totalLabel = + firstYear === undefined || lastYear === undefined + ? 'Total' + : `${firstYear}–${String(lastYear).slice(-2)}` + +export const yearlyChanges = rollingWindow(observations, { + orderBy: 'year', + size: 2, + partial: false, + outputs: { + delta: { value: 'gas', reduce: delta }, + }, +}) + +export const waterfallRows = waterfall(yearlyChanges, { + value: 'delta', + orderBy: 'year', + total: true, +}) + +export const waterfallDefinition = () => + defineChart(({ width }) => { + return { + marks: [ + barY(waterfallRows, { + id: 'waterfall-bars', + x: (datum) => (datum.kind === 'total' ? totalLabel : `${datum.year}`), + y1: 'start', + y2: 'end', + color: 'kind', + key: (datum) => + datum.kind === 'total' ? 'net-total' : `${datum.year}`, + inset: 1, + }), + ruleY([0], { stroke: '#64748b', strokeOpacity: 0.6 }), + ], + x: { + scale: () => scaleBand().padding(0.14), + axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Change in gasoline price (USD per gallon)' }, + }, + color: { + domain: kinds, + range: colors, + legend: colorLegend({ label: 'Change' }), + }, + } + }) + +export const exampleAriaLabel = 'Annual changes in U.S. gasoline prices' + +export const createExampleChart = () => + defineChart(waterfallDefinition(), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + datum.kind === 'total' + ? `${totalLabel} · ${signedAmount.format(datum.end)} net change` + : `${datum.year} · ${signedAmount.format( + datum.end - datum.start, + )} · ${signedAmount.format(datum.end)} running change`, + }, + }, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/29-waterfall/tanstack.test.ts b/benchmarks/conformance/cases/29-waterfall/tanstack.test.ts index c20a956b..68585dad 100644 --- a/benchmarks/conformance/cases/29-waterfall/tanstack.test.ts +++ b/benchmarks/conformance/cases/29-waterfall/tanstack.test.ts @@ -87,7 +87,7 @@ describe('definition-owned waterfall layout', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/29-waterfall/tanstack.ts', + 'benchmarks/conformance/cases/29-waterfall/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/29-waterfall/tanstack.ts b/benchmarks/conformance/cases/29-waterfall/tanstack.ts index a371121c..352e831c 100644 --- a/benchmarks/conformance/cases/29-waterfall/tanstack.ts +++ b/benchmarks/conformance/cases/29-waterfall/tanstack.ts @@ -1,97 +1,11 @@ -import { - barY, - colorLegend, - defineChart, - delta, - ruleY, - rollingWindow, -} from '@tanstack/charts' -import { waterfall } from '@tanstack/charts/transform/waterfall' -import type { WaterfallKind } from '@tanstack/charts/transform/waterfall' -import { scaleBand, scaleLinear } from 'd3-scale' -import { driving } from '@charts-poc/demo-data/driving' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const kinds = [ - 'increase', - 'decrease', - 'total', -] satisfies readonly WaterfallKind[] -const colors = ['#10b981', '#ef4444', '#2563eb'] -const signedAmount = new Intl.NumberFormat('en-US', { - signDisplay: 'always', - style: 'currency', - currency: 'USD', - maximumFractionDigits: 2, -}) +export * from './example' -const observations = driving.filter((row) => row.year >= 2004) -const firstYear = observations[0]?.year -const lastYear = observations.at(-1)?.year -const totalLabel = - firstYear === undefined || lastYear === undefined - ? 'Total' - : `${firstYear}–${String(lastYear).slice(-2)}` - -export const yearlyChanges = rollingWindow(observations, { - orderBy: 'year', - size: 2, - partial: false, - outputs: { - delta: { value: 'gas', reduce: delta }, - }, -}) - -export const waterfallRows = waterfall(yearlyChanges, { - value: 'delta', - orderBy: 'year', - total: true, -}) - -export const waterfallDefinition = () => - defineChart(({ width }) => { - return { - marks: [ - barY(waterfallRows, { - id: 'waterfall-bars', - x: (datum) => (datum.kind === 'total' ? totalLabel : `${datum.year}`), - y1: 'start', - y2: 'end', - color: 'kind', - key: (datum) => - datum.kind === 'total' ? 'net-total' : `${datum.year}`, - inset: 1, - }), - ruleY([0], { stroke: '#64748b', strokeOpacity: 0.6 }), - ], - x: { - scale: () => scaleBand().padding(0.14), - axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Change in gasoline price (USD per gallon)' }, - }, - color: { - domain: kinds, - range: colors, - legend: colorLegend({ label: 'Change' }), - }, - } - }) - -export const catalogCase = tanstackCase( - waterfallDefinition, - 'Annual changes in U.S. gasoline prices', - { - format: ({ datum }) => - datum.kind === 'total' - ? `${totalLabel} · ${signedAmount.format(datum.end)} net change` - : `${datum.year} · ${signedAmount.format( - datum.end - datum.start, - )} · ${signedAmount.format(datum.end)} running change`, - }, +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/30-slopegraph/example.tsx b/benchmarks/conformance/cases/30-slopegraph/example.tsx new file mode 100644 index 00000000..55a0ab73 --- /dev/null +++ b/benchmarks/conformance/cases/30-slopegraph/example.tsx @@ -0,0 +1,117 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { citywages } from '@charts-poc/demo-data/citywages' +import { defineChart, dot, lineY, select, text } from '@tanstack/charts' +import { fold } from '@tanstack/charts/transform/fold' +import { scaleBand, scaleLinear } from 'd3-scale' +import { wageFields, wageYear } from './selection' + +const colors = [ + '#2563eb', + '#f97316', + '#10b981', + '#8b5cf6', + '#ec4899', + '#06b6d4', + '#ca8a04', + '#64748b', +] +const previewLabelOffsets = new Map([ + ['Houston', 7], + ['New York', 12], + ['Los Angeles', 18], + ['Washington, D.C.', 12], + ['Dallas', 6], + ['Chicago', 0], + ['Philadelphia', 12], + ['Miami', 24], +]) + +export const slopegraphDefinition = (input: ExampleOptions) => { + const source = citywages.slice(input.revision * 4, input.revision * 4 + 8) + const rows = fold(source, { + fields: wageFields, + as: { key: 'wageField', value: 'inequality' }, + }) + const labels = select(rows, { + by: 'Metro', + select: 'last', + }) + + return defineChart({ + marks: [ + lineY(rows, { + id: 'metro-lines', + x: ({ wageField }) => wageYear(wageField), + y: 'inequality', + color: 'nyt_display', + key: ({ Metro, wageField }) => `${Metro}:${wageField}`, + }), + dot(rows, { + id: 'metro-points', + x: ({ wageField }) => wageYear(wageField), + y: 'inequality', + color: 'nyt_display', + key: ({ Metro, wageField }) => `${Metro}:${wageField}`, + r: 3, + }), + text(labels, { + id: 'endpoint-labels', + x: ({ wageField }) => wageYear(wageField), + y: 'inequality', + text: 'nyt_display', + color: 'nyt_display', + key: ({ Metro, wageField }) => `${Metro}:${wageField}`, + dx: ({ wageField }) => + input.preview === true && wageField === wageFields[1] ? -6 : 6, + anchor: ({ wageField }) => + input.preview === true && wageField === wageFields[1] + ? 'end' + : 'start', + dy: ({ nyt_display }) => + input.preview === true + ? (previewLabelOffsets.get(nyt_display) ?? 0) + : 0, + fontSize: input.preview === true ? 9 : 12, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.08), + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: '90th/10th percentile wage ratio' }, + }, + color: { + range: colors, + }, + margin: { right: 76 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Metropolitan wage inequality, 1980 to 2015' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(slopegraphDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/30-slopegraph/tanstack.test.ts b/benchmarks/conformance/cases/30-slopegraph/tanstack.test.ts index ea6f86a8..ce7f5264 100644 --- a/benchmarks/conformance/cases/30-slopegraph/tanstack.test.ts +++ b/benchmarks/conformance/cases/30-slopegraph/tanstack.test.ts @@ -68,7 +68,7 @@ describe('folded slopegraph', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/30-slopegraph/tanstack.ts', + 'benchmarks/conformance/cases/30-slopegraph/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/30-slopegraph/tanstack.ts b/benchmarks/conformance/cases/30-slopegraph/tanstack.ts index cd9e31df..6c029e65 100644 --- a/benchmarks/conformance/cases/30-slopegraph/tanstack.ts +++ b/benchmarks/conformance/cases/30-slopegraph/tanstack.ts @@ -1,96 +1,8 @@ -import { citywages } from '@charts-poc/demo-data/citywages' -import { defineChart, dot, lineY, select, text } from '@tanstack/charts' -import { fold } from '@tanstack/charts/transform/fold' -import { scaleBand, scaleLinear } from 'd3-scale' -import { wageFields, wageYear } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#2563eb', - '#f97316', - '#10b981', - '#8b5cf6', - '#ec4899', - '#06b6d4', - '#ca8a04', - '#64748b', -] -const previewLabelOffsets = new Map([ - ['Houston', 7], - ['New York', 12], - ['Los Angeles', 18], - ['Washington, D.C.', 12], - ['Dallas', 6], - ['Chicago', 0], - ['Philadelphia', 12], - ['Miami', 24], -]) +export * from './example' -export const slopegraphDefinition = (input: ConformanceInput) => { - const source = citywages.slice(input.revision * 4, input.revision * 4 + 8) - const rows = fold(source, { - fields: wageFields, - as: { key: 'wageField', value: 'inequality' }, - }) - const labels = select(rows, { - by: 'Metro', - select: 'last', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - lineY(rows, { - id: 'metro-lines', - x: ({ wageField }) => wageYear(wageField), - y: 'inequality', - color: 'nyt_display', - key: ({ Metro, wageField }) => `${Metro}:${wageField}`, - }), - dot(rows, { - id: 'metro-points', - x: ({ wageField }) => wageYear(wageField), - y: 'inequality', - color: 'nyt_display', - key: ({ Metro, wageField }) => `${Metro}:${wageField}`, - r: 3, - }), - text(labels, { - id: 'endpoint-labels', - x: ({ wageField }) => wageYear(wageField), - y: 'inequality', - text: 'nyt_display', - color: 'nyt_display', - key: ({ Metro, wageField }) => `${Metro}:${wageField}`, - dx: ({ wageField }) => - input.preview === true && wageField === wageFields[1] ? -6 : 6, - anchor: ({ wageField }) => - input.preview === true && wageField === wageFields[1] - ? 'end' - : 'start', - dy: ({ nyt_display }) => - input.preview === true - ? (previewLabelOffsets.get(nyt_display) ?? 0) - : 0, - fontSize: input.preview === true ? 9 : 12, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.2).paddingOuter(0.08), - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: '90th/10th percentile wage ratio' }, - }, - color: { - range: colors, - }, - margin: { right: 76 }, - }) -} - -export const mount = tanstackMount( - slopegraphDefinition, - 'Metropolitan wage inequality, 1980 to 2015', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/31-linear-regression/example.tsx b/benchmarks/conformance/cases/31-linear-regression/example.tsx new file mode 100644 index 00000000..a307155d --- /dev/null +++ b/benchmarks/conformance/cases/31-linear-regression/example.tsx @@ -0,0 +1,97 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart, dot, linearRegressionY } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +type CompleteCar = CarsRow & { + 'power (hp)': number + 'economy (mpg)': number +} + +export const completeCars = cars.filter( + (row): row is CompleteCar => + row['power (hp)'] !== null && row['economy (mpg)'] !== null, +) + +function regressionRows(input: ExampleOptions) { + return completeCars.slice(input.revision * 8, input.revision * 8 + 320) +} + +function regressionChart( + rows: readonly CompleteCar[], + scatterRows: readonly CompleteCar[], +) { + return defineChart({ + marks: [ + dot(scatterRows, { + x: 'power (hp)', + y: 'economy (mpg)', + key: (row) => JSON.stringify([row.name, row.year, row['weight (lb)']]), + fill: '#93c5fd', + stroke: '#2563eb', + r: 3, + }), + linearRegressionY(rows, { + id: 'regression', + x: 'power (hp)', + y: 'economy (mpg)', + ci: 0, + stroke: '#dc2626', + strokeWidth: 2, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Power (hp)' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + }) +} + +export const regressionDefinition = (input: ExampleOptions) => { + const rows = regressionRows(input) + return regressionChart(rows, rows) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Scatterplot with linear regression' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(regressionDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'name' in datum + ? `${datum.name} · ${datum['power (hp)'].toLocaleString( + 'en-US', + )} hp · ${datum['economy (mpg)'].toLocaleString('en-US')} mpg` + : `Regression · ${datum.x.toLocaleString( + 'en-US', + )} hp · predicted ${datum.y.toLocaleString('en-US', { + maximumFractionDigits: 1, + })} mpg`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/31-linear-regression/tanstack.test.ts b/benchmarks/conformance/cases/31-linear-regression/tanstack.test.ts index 5da4869a..adcb5c1d 100644 --- a/benchmarks/conformance/cases/31-linear-regression/tanstack.test.ts +++ b/benchmarks/conformance/cases/31-linear-regression/tanstack.test.ts @@ -74,7 +74,7 @@ describe('definition-owned linear regression', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/31-linear-regression/tanstack.ts', + 'benchmarks/conformance/cases/31-linear-regression/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/31-linear-regression/tanstack.ts b/benchmarks/conformance/cases/31-linear-regression/tanstack.ts index 83309037..6c029e65 100644 --- a/benchmarks/conformance/cases/31-linear-regression/tanstack.ts +++ b/benchmarks/conformance/cases/31-linear-regression/tanstack.ts @@ -1,92 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dot, linearRegressionY } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CompleteCar = CarsRow & { - 'power (hp)': number - 'economy (mpg)': number -} +export * from './example' -export const completeCars = cars.filter( - (row): row is CompleteCar => - row['power (hp)'] !== null && row['economy (mpg)'] !== null, -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -function regressionRows(input: ConformanceInput) { - return completeCars.slice(input.revision * 8, input.revision * 8 + 320) -} - -function regressionChart( - rows: readonly CompleteCar[], - scatterRows: readonly CompleteCar[], -) { - return defineChart({ - marks: [ - dot(scatterRows, { - x: 'power (hp)', - y: 'economy (mpg)', - key: (row) => JSON.stringify([row.name, row.year, row['weight (lb)']]), - fill: '#93c5fd', - stroke: '#2563eb', - r: 3, - }), - linearRegressionY(rows, { - id: 'regression', - x: 'power (hp)', - y: 'economy (mpg)', - ci: 0, - stroke: '#dc2626', - strokeWidth: 2, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Power (hp)' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - }) -} - -export const regressionDefinition = (input: ConformanceInput) => { - const rows = regressionRows(input) - return regressionChart(rows, rows) -} - -const catalogRegressionDefinition = (input: ConformanceInput) => { - const rows = regressionRows(input) - return regressionChart( - rows, - samplePreviewData(rows, input, 80, [ - (row) => row['power (hp)'], - (row) => row['economy (mpg)'], - ]), - ) -} - -export const mount = tanstackMount( - regressionDefinition, - 'Scatterplot with linear regression', - { - format: ({ datum }) => - 'name' in datum - ? `${datum.name} · ${datum['power (hp)'].toLocaleString( - 'en-US', - )} hp · ${datum['economy (mpg)'].toLocaleString('en-US')} mpg` - : `Regression · ${datum.x.toLocaleString( - 'en-US', - )} hp · predicted ${datum.y.toLocaleString('en-US', { - maximumFractionDigits: 1, - })} mpg`, - }, -) - -export const catalogCase = tanstackCase( - catalogRegressionDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/32-change-arrows/example.tsx b/benchmarks/conformance/cases/32-change-arrows/example.tsx new file mode 100644 index 00000000..135e592f --- /dev/null +++ b/benchmarks/conformance/cases/32-change-arrows/example.tsx @@ -0,0 +1,63 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { arrow, defineChart } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { citywages } from '@charts-poc/demo-data/citywages' + +const metroChanges = citywages.filter( + (row) => + row.highlight === 1 || + row.R90_10_2015 < row.R90_10_1980 || + row.nyt_display === 'Los Angeles', +) + +export const definition = () => { + const increases = metroChanges.filter( + (row) => row.R90_10_2015 >= row.R90_10_1980, + ) + const decreases = metroChanges.filter( + (row) => row.R90_10_2015 < row.R90_10_1980, + ) + return defineChart({ + marks: [ + arrow(increases, { + x1: 'LPOP_1980', + y1: 'R90_10_1980', + x2: 'LPOP_2015', + y2: 'R90_10_2015', + stroke: '#ef4444', + headLength: 8, + }), + arrow(decreases, { + x1: 'LPOP_1980', + y1: 'R90_10_1980', + x2: 'LPOP_2015', + y2: 'R90_10_2015', + stroke: '#10b981', + headLength: 8, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Log₁₀ population' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: '90th-to-10th-percentile wage ratio' }, + }, + }) +} + +export const exampleAriaLabel = + 'Metro population and wage inequality, 1980–2015' + +export const createExampleChart = () => + defineChart(definition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/32-change-arrows/tanstack.ts b/benchmarks/conformance/cases/32-change-arrows/tanstack.ts index cc04adfe..352e831c 100644 --- a/benchmarks/conformance/cases/32-change-arrows/tanstack.ts +++ b/benchmarks/conformance/cases/32-change-arrows/tanstack.ts @@ -1,53 +1,11 @@ -import { arrow, defineChart } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { citywages } from '@charts-poc/demo-data/citywages' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const metroChanges = citywages.filter( - (row) => - row.highlight === 1 || - row.R90_10_2015 < row.R90_10_1980 || - row.nyt_display === 'Los Angeles', -) - -const definition = () => { - const increases = metroChanges.filter( - (row) => row.R90_10_2015 >= row.R90_10_1980, - ) - const decreases = metroChanges.filter( - (row) => row.R90_10_2015 < row.R90_10_1980, - ) - return defineChart({ - marks: [ - arrow(increases, { - x1: 'LPOP_1980', - y1: 'R90_10_1980', - x2: 'LPOP_2015', - y2: 'R90_10_2015', - stroke: '#ef4444', - headLength: 8, - }), - arrow(decreases, { - x1: 'LPOP_1980', - y1: 'R90_10_1980', - x2: 'LPOP_2015', - y2: 'R90_10_2015', - stroke: '#10b981', - headLength: 8, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Log₁₀ population' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: '90th-to-10th-percentile wage ratio' }, - }, - }) -} +export * from './example' -export const catalogCase = tanstackCase( - definition, - 'Metro population and wage inequality, 1980–2015', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/33-difference-chart/example.tsx b/benchmarks/conformance/cases/33-difference-chart/example.tsx new file mode 100644 index 00000000..b5541d29 --- /dev/null +++ b/benchmarks/conformance/cases/33-difference-chart/example.tsx @@ -0,0 +1,72 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { aapl } from '@charts-poc/demo-data/aapl' +import { defineChart, differenceY, rollingWindow } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { formatDifferenceMonth } from './model' + +export const differenceRows = (input: ExampleOptions) => + rollingWindow(aapl.slice(input.revision * 10, input.revision * 10 + 120), { + size: 20, + orderBy: 'Date', + anchor: 'end', + partial: false, + outputs: { + average: { value: 'Close', reduce: 'mean' }, + }, + }) + +export const differenceDefinition = (input: ExampleOptions) => { + const rows = differenceRows(input) + + return defineChart({ + marks: [ + differenceY(rows, { + id: 'difference', + x: 'Date', + y1: 'average', + y2: 'Close', + positiveFill: '#16a34a', + negativeFill: '#dc2626', + fillOpacity: 0.35, + stroke: '#166534', + strokeWidth: 2, + comparisonStroke: '#475569', + comparisonStrokeWidth: 2, + }), + ], + x: { + scale: scaleUtc, + axis: { ticks: { count: 9, format: formatDifferenceMonth } }, + }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, + margin: { top: 20, right: 20, bottom: 30, left: 80 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = + 'Apple closing price versus its twenty-day average' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(differenceDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/33-difference-chart/tanstack.test.ts b/benchmarks/conformance/cases/33-difference-chart/tanstack.test.ts index ef0c216b..65f16ac9 100644 --- a/benchmarks/conformance/cases/33-difference-chart/tanstack.test.ts +++ b/benchmarks/conformance/cases/33-difference-chart/tanstack.test.ts @@ -84,7 +84,7 @@ describe('definition-owned difference chart', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/33-difference-chart/tanstack.ts', + 'benchmarks/conformance/cases/33-difference-chart/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/33-difference-chart/tanstack.ts b/benchmarks/conformance/cases/33-difference-chart/tanstack.ts index 839b2f4f..6c029e65 100644 --- a/benchmarks/conformance/cases/33-difference-chart/tanstack.ts +++ b/benchmarks/conformance/cases/33-difference-chart/tanstack.ts @@ -1,50 +1,8 @@ -import { aapl } from '@charts-poc/demo-data/aapl' -import { defineChart, differenceY, rollingWindow } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { formatDifferenceMonth } from './model' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const differenceRows = (input: ConformanceInput) => - rollingWindow(aapl.slice(input.revision * 10, input.revision * 10 + 120), { - size: 20, - orderBy: 'Date', - anchor: 'end', - partial: false, - outputs: { - average: { value: 'Close', reduce: 'mean' }, - }, - }) +export * from './example' -export const differenceDefinition = (input: ConformanceInput) => { - const rows = differenceRows(input) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - differenceY(rows, { - id: 'difference', - x: 'Date', - y1: 'average', - y2: 'Close', - positiveFill: '#16a34a', - negativeFill: '#dc2626', - fillOpacity: 0.35, - stroke: '#166534', - strokeWidth: 2, - comparisonStroke: '#475569', - comparisonStrokeWidth: 2, - }), - ], - x: { - scale: scaleUtc, - axis: { ticks: { count: 9, format: formatDifferenceMonth } }, - }, - y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, - margin: { top: 20, right: 20, bottom: 30, left: 80 }, - }) -} - -export const mount = tanstackMount( - differenceDefinition, - 'Apple closing price versus its twenty-day average', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx b/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx new file mode 100644 index 00000000..a9293c51 --- /dev/null +++ b/benchmarks/conformance/cases/34-pointer-tooltip/example.tsx @@ -0,0 +1,81 @@ +import { aapl } from '@charts-poc/demo-data/aapl' +import type { AaplRow } from '@charts-poc/demo-data/aapl' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { decorative } from '@tanstack/charts/mark/decorative' +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleLinear, scaleUtc } from 'd3-scale' +import type { ChartTooltipOptions } from '@tanstack/charts' +import { selectPointerTooltipData } from './selection' + +export interface ExampleOptions { + revision?: number +} + +const interactiveTooltip: ChartTooltipOptions = { + anchor: 'point', + placement: ['top', 'right', 'left', 'bottom'], + items: [ + { + channel: 'y', + label: 'Apple', + text: (point) => + point.datum.Close.toLocaleString(undefined, { + maximumFractionDigits: 2, + }), + }, + { channel: 'x', label: 'Date' }, + ], +} + +export function createExampleChart({ revision = 0 }: ExampleOptions = {}) { + const rows = selectPointerTooltipData(aapl, revision) + return defineChart( + { + marks: [ + decorative( + lineY(rows, { + x: 'Date', + y: 'Close', + stroke: '#2563eb', + }), + ), + dot(rows, { + id: 'apple-points', + x: 'Date', + y: 'Close', + fill: '#2563eb', + r: 3, + states: [ + { + when: { focus: 'primary' }, + style: { r: 7, stroke: 'Canvas', strokeWidth: 2 }, + transition: { + type: 'tween', + duration: 140, + easing: 'ease-out', + }, + }, + ], + }), + ], + x: { scale: scaleUtc }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Apple close (USD)' }, + }, + }, + { + keyboard: true, + tooltip: { use: tooltip, ...interactiveTooltip }, + }, + ) +} + +export const exampleAriaLabel = 'Interactive Apple closing price' +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts b/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts index b96c3468..299fcb42 100644 --- a/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts @@ -1,115 +1,42 @@ -import { defineChart, dot, lineY, mountChart } from '@tanstack/charts' -import { decorative } from '@tanstack/charts/mark/decorative' -import type { ChartHostOptions, ChartTooltipOptions } from '@tanstack/charts' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { aapl } from '@charts-poc/demo-data/aapl' +import { mountChart } from '@tanstack/charts' +import type { ChartHostOptions } from '@tanstack/charts' import type { AaplRow } from '@charts-poc/demo-data/aapl' import type { ConformanceHandle, ConformanceInput, ConformanceTestDriver, } from '../../types' -import { selectPointerTooltipData } from './selection' -import { tanstackCase } from '../../shared/mount' +import { tanstackExampleMount } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' -const catalogPreviewDate = '2013-06-06' +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = selectPointerTooltipData(aapl, input.revision) - return defineChart({ - marks: [ - decorative( - lineY(rows, { - x: 'Date', - y: 'Close', - stroke: '#2563eb', - }), - ), - dot(rows, { - id: 'apple-points', - x: 'Date', - y: 'Close', - fill: '#2563eb', - r: 3, - states: [ - { - when: { focus: 'primary' }, - style: { - r: 7, - stroke: 'Canvas', - strokeWidth: 2, - }, - transition: { - type: 'tween', - duration: 140, - easing: 'ease-out', - }, - }, - ], - }), - ], - x: { scale: scaleUtc }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, - }) -} - -const interactiveTooltip: ChartTooltipOptions = { - anchor: 'point', - placement: ['top', 'right', 'left', 'bottom'], - items: [ - { - channel: 'y', - label: 'Apple', - text: (point) => - point.datum.Close.toLocaleString(undefined, { - maximumFractionDigits: 2, - }), - }, - { - channel: 'x', - label: 'Date', - }, - ], -} - -export const catalogCase = tanstackCase( - definition, - 'Interactive Apple closing price', - interactiveTooltip, +export const catalogCase = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { focus(scene) { return ( scene.points.find( (point) => point.markId === 'apple-points' && - dateKey(point.datum.Date) === catalogPreviewDate, + dateKey(point.datum.Date) === '2013-06-06', ) ?? null ) }, }, ) -const configuredDefinition = (input: ConformanceInput) => - defineChart(definition(input), { - svgAnimation: false, - keyboard: true, - tooltip: { - use: tooltip, - ...interactiveTooltip, - }, - }) - export function mount( container: HTMLElement, input: ConformanceInput, ): ConformanceHandle { let focusedDates: string[] = [] const options: ChartHostOptions = { - definition: configuredDefinition(input), + definition: createExampleChart(input), width: input.width, height: input.height, - ariaLabel: 'Interactive Apple closing price', + ariaLabel: exampleAriaLabel, onFocusGroupChange(points) { focusedDates = points.map((point) => dateKey(point.datum.Date)) }, @@ -159,7 +86,7 @@ export function mount( update(nextInput) { host.update({ ...options, - definition: configuredDefinition(nextInput), + definition: createExampleChart(nextInput), width: nextInput.width, height: nextInput.height, }) diff --git a/benchmarks/conformance/cases/35-grouped-tooltip/example.tsx b/benchmarks/conformance/cases/35-grouped-tooltip/example.tsx new file mode 100644 index 00000000..7d052181 --- /dev/null +++ b/benchmarks/conformance/cases/35-grouped-tooltip/example.tsx @@ -0,0 +1,82 @@ +import { industries } from '@charts-poc/demo-data/industries' +import { bandX, defineChart, dot, lineY, whenFocused } from '@tanstack/charts' +import { decorative } from '@tanstack/charts/mark/decorative' +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { portal } from '@tanstack/charts/tooltip/portal' +import { scaleLinear, scaleUtc } from 'd3-scale' +import type { ChartTooltipOptions } from '@tanstack/charts' +import { industryNames, selectGroupedTooltipData } from './selection' +import type { GroupedTooltipDatum } from './selection' + +export interface ExampleOptions { + revision?: number +} + +const colors = ['#2563eb', '#f97316', '#10b981'] +const interactiveTooltip: ChartTooltipOptions = { + portal, + anchor: 'group-center', + placement: ['top', 'right', 'left', 'bottom'], + sort: 'color-domain', +} + +export function createExampleChart({ revision = 0 }: ExampleOptions = {}) { + const rows = selectGroupedTooltipData(industries, revision) + const dates = rows.filter((row) => row.industry === industryNames[0]) + return defineChart( + { + marks: [ + whenFocused( + bandX(dates, { + id: 'focus-date-band', + x: 'date', + fill: '#64748b', + fillOpacity: 0.14, + inset: 3, + radius: 4, + }), + { match: 'x' }, + ), + decorative( + lineY(rows, { x: 'date', y: 'unemployed', color: 'industry' }), + ), + dot(rows, { + id: 'grouped-points', + x: 'date', + y: 'unemployed', + z: 'industry', + color: 'industry', + r: 2.5, + states: [ + { + when: { focus: 'group' }, + style: { r: 5, stroke: 'Canvas', strokeWidth: 1.5 }, + transition: { type: 'tween', duration: 140, easing: 'ease-out' }, + }, + { when: { focus: 'unmatched' }, style: { opacity: 0.3 } }, + ], + }), + ], + x: { scale: scaleUtc }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Unemployed (thousands)' }, + }, + color: { domain: industryNames, range: colors }, + }, + { + keyboard: true, + focus: 'group-x', + tooltip: { use: tooltip, ...interactiveTooltip }, + }, + ) +} + +export const exampleAriaLabel = 'Grouped industry unemployment tooltip' +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts b/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts index 19156821..4dca4f4d 100644 --- a/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts @@ -1,103 +1,20 @@ -import { - bandX, - defineChart, - dot, - lineY, - mountChart, - whenFocused, -} from '@tanstack/charts' -import { decorative } from '@tanstack/charts/mark/decorative' -import type { ChartHostOptions, ChartTooltipOptions } from '@tanstack/charts' -import { tooltip } from '@tanstack/charts/tooltip' -import { portal } from '@tanstack/charts/tooltip/portal' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' +import { mountChart } from '@tanstack/charts' +import type { ChartHostOptions } from '@tanstack/charts' import type { ConformanceHandle, ConformanceInput, ConformanceTestDriver, } from '../../types' -import { industryNames, selectGroupedTooltipData } from './selection' +import { industryNames } from './selection' import type { GroupedTooltipDatum } from './selection' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#f97316', '#10b981'] -const catalogPreviewDate = '2001-07-01' +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = selectGroupedTooltipData(industries, input.revision) - const dates = rows.filter((row) => row.industry === industryNames[0]) - return defineChart({ - marks: [ - whenFocused( - bandX(dates, { - id: 'focus-date-band', - x: 'date', - fill: '#64748b', - fillOpacity: 0.14, - inset: 3, - radius: 4, - }), - { match: 'x' }, - ), - decorative( - lineY(rows, { - x: 'date', - y: 'unemployed', - color: 'industry', - }), - ), - dot(rows, { - id: 'grouped-points', - x: 'date', - y: 'unemployed', - z: 'industry', - color: 'industry', - r: 2.5, - states: [ - { - when: { focus: 'group' }, - style: { r: 5, stroke: 'Canvas', strokeWidth: 1.5 }, - transition: { - type: 'tween', - duration: 140, - easing: 'ease-out', - }, - }, - { - when: { focus: 'unmatched' }, - style: { opacity: 0.3 }, - }, - ], - }), - ], - x: { scale: scaleUtc }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Unemployed (thousands)' }, - }, - color: { - domain: industryNames, - range: colors, - }, - }) -} - -const interactiveTooltip: ChartTooltipOptions = { - portal, - anchor: 'group-center', - placement: ['top', 'right', 'left', 'bottom'], - sort: 'color-domain', -} - -const catalogDefinition = (input: ConformanceInput) => - defineChart(definition(input), { focus: 'group-x' }) - -export const catalogCase = tanstackCase( - catalogDefinition, - 'Grouped industry unemployment tooltip', - interactiveTooltip, +export const catalogCase = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { focus(scene) { return ( @@ -105,34 +22,23 @@ export const catalogCase = tanstackCase( (point) => point.markId === 'grouped-points' && point.datum.industry === industryNames[0] && - dateKey(point.datum.date) === catalogPreviewDate, + dateKey(point.datum.date) === '2001-07-01', ) ?? null ) }, }, ) -const configuredDefinition = (input: ConformanceInput) => - defineChart(definition(input), { - svgAnimation: false, - keyboard: true, - focus: 'group-x', - tooltip: { - use: tooltip, - ...interactiveTooltip, - }, - }) - export function mount( container: HTMLElement, input: ConformanceInput, ): ConformanceHandle { let focusedRows: string[] = [] const options: ChartHostOptions = { - definition: configuredDefinition(input), + definition: createExampleChart(input), width: input.width, height: input.height, - ariaLabel: 'Grouped industry unemployment tooltip', + ariaLabel: exampleAriaLabel, onFocusGroupChange(points) { focusedRows = points.map( (point) => `${dateKey(point.datum.date)}:${point.datum.industry}`, @@ -180,7 +86,7 @@ export function mount( update(nextInput) { host.update({ ...options, - definition: configuredDefinition(nextInput), + definition: createExampleChart(nextInput), width: nextInput.width, height: nextInput.height, }) diff --git a/benchmarks/conformance/cases/36-hierarchy-tree/example.tsx b/benchmarks/conformance/cases/36-hierarchy-tree/example.tsx new file mode 100644 index 00000000..e652906b --- /dev/null +++ b/benchmarks/conformance/cases/36-hierarchy-tree/example.tsx @@ -0,0 +1,103 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, dot, link, text } from '@tanstack/charts' +import { treeLayout } from '@tanstack/charts/hierarchy/tree' +import { flare } from '@charts-poc/demo-data/flare' +import { scaleLinear } from 'd3-scale' +import { selectHierarchyData } from './selection' + +export const treeDefinition = (input: ExampleOptions) => { + const layout = treeLayout(selectHierarchyData(flare, input.revision), { + path: 'name', + delimiter: '.', + }) + + return defineChart({ + marks: [ + link(layout.links, { + id: 'hierarchy-links', + x1: 'x1', + y1: 'y1', + x2: 'x2', + y2: 'y2', + key: 'id', + stroke: '#94a3b8', + strokeOpacity: 0.55, + strokeWidth: 1.5, + }), + dot(layout.nodes, { + id: 'hierarchy-nodes', + x: 'x', + y: 'y', + key: 'id', + fill: '#2563eb', + r: 3.5, + }), + text(layout.nodes, { + id: 'hierarchy-labels', + x: 'x', + y: 'y', + text: 'name', + key: 'id', + fill: '#2563eb', + fontSize: input.preview === true ? 8 : 10, + anchor: (node) => + input.preview === true + ? node.internal + ? 'start' + : 'end' + : node.internal + ? 'end' + : 'start', + dx: (node) => + input.preview === true + ? node.internal + ? 4 + : -4 + : node.internal + ? -6 + : 6, + dy: (node) => + input.preview !== true ? 0 : node.y >= 1 ? 5 : node.y <= 0 ? -5 : 0, + }), + ], + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + guides: false, + margin: { top: 22, right: 140, bottom: 22, left: 50 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Tidy Flare analytics hierarchy' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(treeDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'internal' in datum + ? `${datum.data?.name ?? datum.name} · ${datum.internal ? 'Group' : 'Leaf'}` + : `${datum.sourceNode.data?.name ?? datum.source} → ${datum.targetNode.data?.name ?? datum.target}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.test.ts b/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.test.ts index 6e118467..5f083224 100644 --- a/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.test.ts +++ b/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.test.ts @@ -142,7 +142,7 @@ describe('native hierarchy tree', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts', + 'benchmarks/conformance/cases/36-hierarchy-tree/example.tsx', ), 'utf8', ) @@ -159,7 +159,6 @@ function render(revision: number) { treeDefinition({ ...size, revision, - interactive: true, } satisfies ConformanceInput), size, ) diff --git a/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts b/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts index c8daf194..6c029e65 100644 --- a/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts +++ b/benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts @@ -1,80 +1,8 @@ -import { defineChart, dot, link, text } from '@tanstack/charts' -import { treeLayout } from '@tanstack/charts/hierarchy/tree' -import { flare } from '@charts-poc/demo-data/flare' -import { scaleLinear } from 'd3-scale' -import { selectHierarchyData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const treeDefinition = (input: ConformanceInput) => { - const layout = treeLayout(selectHierarchyData(flare, input.revision), { - path: 'name', - delimiter: '.', - }) +export * from './example' - return defineChart({ - marks: [ - link(layout.links, { - id: 'hierarchy-links', - x1: 'x1', - y1: 'y1', - x2: 'x2', - y2: 'y2', - key: 'id', - stroke: '#94a3b8', - strokeOpacity: 0.55, - strokeWidth: 1.5, - }), - dot(layout.nodes, { - id: 'hierarchy-nodes', - x: 'x', - y: 'y', - key: 'id', - fill: '#2563eb', - r: 3.5, - }), - text(layout.nodes, { - id: 'hierarchy-labels', - x: 'x', - y: 'y', - text: 'name', - key: 'id', - fill: '#2563eb', - fontSize: input.preview === true ? 8 : 10, - anchor: (node) => - input.preview === true - ? node.internal - ? 'start' - : 'end' - : node.internal - ? 'end' - : 'start', - dx: (node) => - input.preview === true - ? node.internal - ? 4 - : -4 - : node.internal - ? -6 - : 6, - dy: (node) => - input.preview !== true ? 0 : node.y >= 1 ? 5 : node.y <= 0 ? -5 : 0, - }), - ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, - guides: false, - margin: { top: 22, right: 140, bottom: 22, left: 50 }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - treeDefinition, - 'Tidy Flare analytics hierarchy', - { - format: ({ datum }) => - 'internal' in datum - ? `${datum.data?.name ?? datum.name} · ${datum.internal ? 'Group' : 'Leaf'}` - : `${datum.sourceNode.data?.name ?? datum.source} → ${datum.targetNode.data?.name ?? datum.target}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/37-delaunay-network/example.tsx b/benchmarks/conformance/cases/37-delaunay-network/example.tsx new file mode 100644 index 00000000..3a8e1ff6 --- /dev/null +++ b/benchmarks/conformance/cases/37-delaunay-network/example.tsx @@ -0,0 +1,70 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart, dot } from '@tanstack/charts' +import { delaunayLink } from '@tanstack/charts/spatial/delaunay' +import { scaleLinear } from 'd3-scale' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +const carKey = (row: CarsRow) => `${row.name}:${row.year}:${row['weight (lb)']}` + +export const definition = (input: ExampleOptions) => { + const points = cars + .filter((row) => { + return row['economy (mpg)'] !== null && row['power (hp)'] !== null + }) + .slice(input.revision * 3, input.revision * 3 + 24) + return defineChart({ + marks: [ + delaunayLink(points, { + x: 'weight (lb)', + y: 'economy (mpg)', + key: carKey, + stroke: '#94a3b8', + strokeOpacity: 0.75, + strokeWidth: 1, + }), + dot(points, { + x: 'weight (lb)', + y: 'economy (mpg)', + key: carKey, + fill: '#2563eb', + stroke: '#ffffff', + strokeWidth: 1, + r: 4, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Delaunay spatial network' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts b/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts index 3f11b83f..6c029e65 100644 --- a/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts +++ b/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts @@ -1,46 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dot } from '@tanstack/charts' -import { delaunayLink } from '@tanstack/charts/spatial/delaunay' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const carKey = (row: CarsRow) => `${row.name}:${row.year}:${row['weight (lb)']}` +export * from './example' -const definition = (input: ConformanceInput) => { - const points = cars - .filter((row) => { - return row['economy (mpg)'] !== null && row['power (hp)'] !== null - }) - .slice(input.revision * 3, input.revision * 3 + 24) - return defineChart({ - marks: [ - delaunayLink(points, { - x: 'weight (lb)', - y: 'economy (mpg)', - key: carKey, - stroke: '#94a3b8', - strokeOpacity: 0.75, - strokeWidth: 1, - }), - dot(points, { - x: 'weight (lb)', - y: 'economy (mpg)', - key: carKey, - fill: '#2563eb', - stroke: '#ffffff', - strokeWidth: 1, - r: 4, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount(definition, 'Delaunay spatial network') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/38-contour-topography/example.tsx b/benchmarks/conformance/cases/38-contour-topography/example.tsx new file mode 100644 index 00000000..f28c7af1 --- /dev/null +++ b/benchmarks/conformance/cases/38-contour-topography/example.tsx @@ -0,0 +1,57 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { contour } from '@tanstack/charts/spatial/contour' +import { scaleThreshold } from 'd3-scale' +import { contourThresholds, windObservationGrid } from './transform' + +const colors = ['#dbeafe', '#bfdbfe', '#93c5fd', '#60a5fa', '#2563eb'] + +export const contourDefinition = (input: ExampleOptions) => { + const grid = windObservationGrid(input.revision) + + return defineChart({ + marks: [ + contour(grid.data, { + width: grid.width, + height: grid.height, + value: (row) => Math.hypot(row.u, row.v), + thresholds: contourThresholds, + stroke: '#ffffff', + strokeWidth: 0.75, + }), + ], + color: { + scale: scaleThreshold, + domain: contourThresholds.slice(1), + range: colors, + }, + margin: 12, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Filled wind-speed contours' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(contourDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/38-contour-topography/tanstack.test.ts b/benchmarks/conformance/cases/38-contour-topography/tanstack.test.ts index bf2a4fbb..24a16f74 100644 --- a/benchmarks/conformance/cases/38-contour-topography/tanstack.test.ts +++ b/benchmarks/conformance/cases/38-contour-topography/tanstack.test.ts @@ -12,7 +12,6 @@ describe('native scalar-grid contours', () => { width: 640, height: 400, revision: 0, - interactive: true, } satisfies ConformanceInput), { width: 640, height: 400 }, ) diff --git a/benchmarks/conformance/cases/38-contour-topography/tanstack.ts b/benchmarks/conformance/cases/38-contour-topography/tanstack.ts index e0d539f8..6c029e65 100644 --- a/benchmarks/conformance/cases/38-contour-topography/tanstack.ts +++ b/benchmarks/conformance/cases/38-contour-topography/tanstack.ts @@ -1,36 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { contour } from '@tanstack/charts/spatial/contour' -import { scaleThreshold } from 'd3-scale' -import { contourThresholds, windObservationGrid } from './transform' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#dbeafe', '#bfdbfe', '#93c5fd', '#60a5fa', '#2563eb'] +export * from './example' -export const contourDefinition = (input: ConformanceInput) => { - const grid = windObservationGrid(input.revision) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - contour(grid.data, { - width: grid.width, - height: grid.height, - value: (row) => Math.hypot(row.u, row.v), - thresholds: contourThresholds, - stroke: '#ffffff', - strokeWidth: 0.75, - }), - ], - color: { - scale: scaleThreshold, - domain: contourThresholds.slice(1), - range: colors, - }, - margin: 12, - }) -} - -export const mount = tanstackMount( - contourDefinition, - 'Filled wind-speed contours', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/39-density-contours/example.tsx b/benchmarks/conformance/cases/39-density-contours/example.tsx new file mode 100644 index 00000000..541241e2 --- /dev/null +++ b/benchmarks/conformance/cases/39-density-contours/example.tsx @@ -0,0 +1,74 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { defineChart } from '@tanstack/charts' +import { densityContour } from '@tanstack/charts/spatial/density' +import { scaleLinear } from 'd3-scale' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +type PenguinBill = PenguinsRow & { + readonly culmen_length_mm: number + readonly culmen_depth_mm: number +} + +const densityBandwidth = 18 +export const densityThresholds = [0.0004, 0.0008, 0.0012, 0.0016, 0.002, 0.0024] +export const densityXDomain: [number, number] = [30, 62] +export const densityYDomain: [number, number] = [12, 23] + +export const densityDefinition = (input: ExampleOptions) => { + const points = penguins + .filter((row): row is PenguinBill => { + return row.culmen_length_mm !== null && row.culmen_depth_mm !== null + }) + .slice(input.revision * 8, input.revision * 8 + 320) + + return defineChart({ + marks: [ + densityContour(points, { + x: 'culmen_length_mm', + y: 'culmen_depth_mm', + bandwidth: densityBandwidth, + thresholds: densityThresholds, + fill: '#2563eb', + fillOpacity: 0.16, + stroke: '#1e3a8a', + strokeWidth: 1, + }), + ], + x: { + scale: scaleLinear().domain(densityXDomain), + }, + y: { + scale: scaleLinear().domain(densityYDomain), + }, + guides: false, + margin: densityBandwidth * 1.5, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Point density contours' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(densityDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/39-density-contours/tanstack.test.ts b/benchmarks/conformance/cases/39-density-contours/tanstack.test.ts index 2fa20ca5..c0cecc5e 100644 --- a/benchmarks/conformance/cases/39-density-contours/tanstack.test.ts +++ b/benchmarks/conformance/cases/39-density-contours/tanstack.test.ts @@ -12,7 +12,6 @@ describe('native density contours', () => { width: 640, height: 400, revision: 0, - interactive: true, } satisfies ConformanceInput), { width: 640, height: 400 }, ) diff --git a/benchmarks/conformance/cases/39-density-contours/tanstack.ts b/benchmarks/conformance/cases/39-density-contours/tanstack.ts index 438c7002..6c029e65 100644 --- a/benchmarks/conformance/cases/39-density-contours/tanstack.ts +++ b/benchmarks/conformance/cases/39-density-contours/tanstack.ts @@ -1,50 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { defineChart } from '@tanstack/charts' -import { densityContour } from '@tanstack/charts/spatial/density' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type PenguinBill = PenguinsRow & { - readonly culmen_length_mm: number - readonly culmen_depth_mm: number -} +export * from './example' -const densityBandwidth = 18 -export const densityThresholds = [0.0004, 0.0008, 0.0012, 0.0016, 0.002, 0.0024] -export const densityXDomain: [number, number] = [30, 62] -export const densityYDomain: [number, number] = [12, 23] +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const densityDefinition = (input: ConformanceInput) => { - const points = penguins - .filter((row): row is PenguinBill => { - return row.culmen_length_mm !== null && row.culmen_depth_mm !== null - }) - .slice(input.revision * 8, input.revision * 8 + 320) - - return defineChart({ - marks: [ - densityContour(points, { - x: 'culmen_length_mm', - y: 'culmen_depth_mm', - bandwidth: densityBandwidth, - thresholds: densityThresholds, - fill: '#2563eb', - fillOpacity: 0.16, - stroke: '#1e3a8a', - strokeWidth: 1, - }), - ], - x: { - scale: scaleLinear().domain(densityXDomain), - }, - y: { - scale: scaleLinear().domain(densityYDomain), - }, - guides: false, - margin: densityBandwidth * 1.5, - }) -} - -export const mount = tanstackMount(densityDefinition, 'Point density contours') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/40-force-directed-network/example.tsx b/benchmarks/conformance/cases/40-force-directed-network/example.tsx new file mode 100644 index 00000000..c46cc7e7 --- /dev/null +++ b/benchmarks/conformance/cases/40-force-directed-network/example.tsx @@ -0,0 +1,119 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, dot, link, text } from '@tanstack/charts' +import { forceLayout } from '@tanstack/charts/network/force' +import { miserables } from '@charts-poc/demo-data/miserables' +import { scaleLinear } from 'd3-scale' +import { forceNetworkData } from './transform' + +const colors = ['#2563eb', '#f97316', '#10b981'] +const network = forceNetworkData(miserables) + +export const forceDefinition = (input: ExampleOptions) => { + const distanceDelta = Math.abs(input.revision % 2) * 3 + const graph = forceLayout(network.nodes, network.links, { + nodeKey: 'id', + source: 'source', + target: 'target', + iterations: 300, + domainPadding: 0.2, + forces: [ + { + type: 'link', + distance: (datum) => + 54 - Math.min(datum.value, 10) * 1.8 + distanceDelta, + strength: (datum) => 0.2 + Math.min(datum.value, 10) * 0.045, + }, + { type: 'manyBody', strength: -165 }, + { type: 'center', x: 0, y: 0 }, + { type: 'collide', radius: 15, strength: 0.9 }, + { type: 'x', x: 0, strength: 0.035 }, + { type: 'y', y: 0, strength: 0.035 }, + ], + }) + + return defineChart({ + marks: [ + link(graph.links, { + id: 'network-links', + x1: 'x1', + y1: 'y1', + x2: 'x2', + y2: 'y2', + key: ({ source, target }) => `${source}->${target}`, + stroke: '#94a3b8', + strokeOpacity: 0.6, + strokeWidth: 2, + }), + dot(graph.nodes, { + id: 'network-nodes', + x: 'x', + y: 'y', + color: 'group', + key: 'id', + r: 7, + stroke: '#ffffff', + strokeWidth: 1.5, + }), + ...(input.preview === true + ? [] + : [ + text(graph.nodes, { + id: 'network-labels', + x: 'x', + y: 'y', + text: 'id', + key: 'id', + dy: -12, + fontSize: 10, + fontWeight: 600, + }), + ]), + ], + x: { + scale: scaleLinear().domain(graph.xDomain), + }, + y: { + scale: scaleLinear().domain(graph.yDomain), + }, + guides: false, + color: { + range: colors, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = + 'Force-directed Les Misérables character network' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(forceDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'group' in datum + ? `${datum.id} · Group ${datum.group}` + : `${datum.source} → ${datum.target} · Value ${datum.value}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/40-force-directed-network/tanstack.test.ts b/benchmarks/conformance/cases/40-force-directed-network/tanstack.test.ts index e23d140d..c7d8b246 100644 --- a/benchmarks/conformance/cases/40-force-directed-network/tanstack.test.ts +++ b/benchmarks/conformance/cases/40-force-directed-network/tanstack.test.ts @@ -165,7 +165,7 @@ describe('native force-directed network', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/40-force-directed-network/tanstack.ts', + 'benchmarks/conformance/cases/40-force-directed-network/example.tsx', ), 'utf8', ) @@ -181,7 +181,6 @@ function render(revision: number) { forceDefinition({ ...size, revision, - interactive: true, } satisfies ConformanceInput), size, ) diff --git a/benchmarks/conformance/cases/40-force-directed-network/tanstack.ts b/benchmarks/conformance/cases/40-force-directed-network/tanstack.ts index 9d3a5ee8..6c029e65 100644 --- a/benchmarks/conformance/cases/40-force-directed-network/tanstack.ts +++ b/benchmarks/conformance/cases/40-force-directed-network/tanstack.ts @@ -1,97 +1,8 @@ -import { defineChart, dot, link, text } from '@tanstack/charts' -import { forceLayout } from '@tanstack/charts/network/force' -import { miserables } from '@charts-poc/demo-data/miserables' -import { scaleLinear } from 'd3-scale' -import { forceNetworkData } from './transform' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#f97316', '#10b981'] -const network = forceNetworkData(miserables) +export * from './example' -export const forceDefinition = (input: ConformanceInput) => { - const distanceDelta = Math.abs(input.revision % 2) * 3 - const graph = forceLayout(network.nodes, network.links, { - nodeKey: 'id', - source: 'source', - target: 'target', - iterations: 300, - domainPadding: 0.2, - forces: [ - { - type: 'link', - distance: (datum) => - 54 - Math.min(datum.value, 10) * 1.8 + distanceDelta, - strength: (datum) => 0.2 + Math.min(datum.value, 10) * 0.045, - }, - { type: 'manyBody', strength: -165 }, - { type: 'center', x: 0, y: 0 }, - { type: 'collide', radius: 15, strength: 0.9 }, - { type: 'x', x: 0, strength: 0.035 }, - { type: 'y', y: 0, strength: 0.035 }, - ], - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - link(graph.links, { - id: 'network-links', - x1: 'x1', - y1: 'y1', - x2: 'x2', - y2: 'y2', - key: ({ source, target }) => `${source}->${target}`, - stroke: '#94a3b8', - strokeOpacity: 0.6, - strokeWidth: 2, - }), - dot(graph.nodes, { - id: 'network-nodes', - x: 'x', - y: 'y', - color: 'group', - key: 'id', - r: 7, - stroke: '#ffffff', - strokeWidth: 1.5, - }), - ...(input.preview === true - ? [] - : [ - text(graph.nodes, { - id: 'network-labels', - x: 'x', - y: 'y', - text: 'id', - key: 'id', - dy: -12, - fontSize: 10, - fontWeight: 600, - }), - ]), - ], - x: { - scale: scaleLinear().domain(graph.xDomain), - }, - y: { - scale: scaleLinear().domain(graph.yDomain), - }, - guides: false, - color: { - range: colors, - }, - }) -} - -export const catalogCase = tanstackCase( - forceDefinition, - 'Force-directed Les Misérables character network', - { - format: ({ datum }) => - 'group' in datum - ? `${datum.id} · Group ${datum.group}` - : `${datum.source} → ${datum.target} · Value ${datum.value}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/40-geojson-map/example.tsx b/benchmarks/conformance/cases/40-geojson-map/example.tsx new file mode 100644 index 00000000..aef58817 --- /dev/null +++ b/benchmarks/conformance/cases/40-geojson-map/example.tsx @@ -0,0 +1,59 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { westportHouse } from '@charts-poc/demo-data/westport-house' +import { defineChart } from '@tanstack/charts' +import { geoShape } from '@tanstack/charts/geo' +import { geoIdentity } from 'd3-geo' + +const strokes = ['#1e293b', '#2563eb'] + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + geoShape(westportHouse.features, { + key: (feature) => feature.properties.id, + projection: { + type: geoIdentity, + fit: westportHouse, + }, + fill: 'none', + stroke: strokes[input.revision % 2] ?? strokes[0], + strokeWidth: 1, + }), + ], + margin: 10, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Westport House floor plan' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + datum.properties.name ?? + datum.properties.roomnumber ?? + datum.properties.type.replaceAll('_', ' '), + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/40-geojson-map/tanstack.ts b/benchmarks/conformance/cases/40-geojson-map/tanstack.ts index 15884448..6c029e65 100644 --- a/benchmarks/conformance/cases/40-geojson-map/tanstack.ts +++ b/benchmarks/conformance/cases/40-geojson-map/tanstack.ts @@ -1,32 +1,8 @@ -import { westportHouse } from '@charts-poc/demo-data/westport-house' -import { defineChart } from '@tanstack/charts' -import { geoShape } from '@tanstack/charts/geo' -import { geoIdentity } from 'd3-geo' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const strokes = ['#1e293b', '#2563eb'] +export * from './example' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - geoShape(westportHouse.features, { - key: (feature) => feature.properties.id, - projection: { - type: geoIdentity, - fit: westportHouse, - }, - fill: 'none', - stroke: strokes[input.revision % 2] ?? strokes[0], - strokeWidth: 1, - }), - ], - margin: 10, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount(definition, 'Westport House floor plan', { - format: ({ datum }) => - datum.properties.name ?? - datum.properties.roomnumber ?? - datum.properties.type.replaceAll('_', ' '), -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/41-waffle-unit-chart/example.tsx b/benchmarks/conformance/cases/41-waffle-unit-chart/example.tsx new file mode 100644 index 00000000..aefa616b --- /dev/null +++ b/benchmarks/conformance/cases/41-waffle-unit-chart/example.tsx @@ -0,0 +1,52 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { colorLegend, defineChart, waffleY } from '@tanstack/charts' +import { alphabet } from '@charts-poc/demo-data/alphabet' + +const colors = [ + '#8b5cf6', + '#10b981', + '#ec4899', + '#f97316', + '#2563eb', + '#06b6d4', +] +const letters = alphabet.map((row) => row.letter) + +const waffleDefinition = (showLegend: boolean) => + defineChart({ + marks: [ + waffleY(alphabet, { + y: 'frequency', + color: 'letter', + key: 'letter', + unit: 0.01, + gap: 2, + round: true, + radius: 2, + }), + ], + guides: false, + color: { + domain: letters, + range: colors, + ...(showLegend ? { legend: colorLegend({ label: 'Letter' }) } : {}), + }, + }) + +export const definition = () => waffleDefinition(true) + +export const exampleAriaLabel = 'English letter frequency waffle chart' + +export const createExampleChart = () => + defineChart(definition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/41-waffle-unit-chart/tanstack.ts b/benchmarks/conformance/cases/41-waffle-unit-chart/tanstack.ts index 5a7689c3..352e831c 100644 --- a/benchmarks/conformance/cases/41-waffle-unit-chart/tanstack.ts +++ b/benchmarks/conformance/cases/41-waffle-unit-chart/tanstack.ts @@ -1,51 +1,11 @@ -import { colorLegend, defineChart, waffleY } from '@tanstack/charts' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#8b5cf6', - '#10b981', - '#ec4899', - '#f97316', - '#2563eb', - '#06b6d4', -] -const letters = alphabet.map((row) => row.letter) +export * from './example' -const waffleDefinition = (showLegend: boolean) => - defineChart({ - marks: [ - waffleY(alphabet, { - y: 'frequency', - color: 'letter', - key: 'letter', - unit: 0.01, - gap: 2, - round: true, - radius: 2, - }), - ], - guides: false, - color: { - domain: letters, - range: colors, - ...(showLegend ? { legend: colorLegend({ label: 'Letter' }) } : {}), - }, - }) - -const definition = () => waffleDefinition(true) - -const catalogDefinition = (input: ConformanceInput) => - waffleDefinition(input.preview !== true) - -export const mount = tanstackMount( - definition, - 'English letter frequency waffle chart', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const catalogCase = tanstackCase( - catalogDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/42-vector-field/example.tsx b/benchmarks/conformance/cases/42-vector-field/example.tsx new file mode 100644 index 00000000..6b77d7d4 --- /dev/null +++ b/benchmarks/conformance/cases/42-vector-field/example.tsx @@ -0,0 +1,40 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, vector } from '@tanstack/charts' +import { wind } from '@charts-poc/demo-data/wind' +import { scaleLinear, scaleSqrt } from 'd3-scale' +import { sampleWind } from './selection' + +const speed = scaleSqrt().domain([0, 14]).range([0, 22]) +const sampledWind = sampleWind(wind) + +export const vectorFieldDefinition = () => + defineChart({ + marks: [ + vector(sampledWind, { + id: 'wind-vectors', + x: 'longitude', + y: 'latitude', + length: (row) => speed(Math.hypot(row.u, row.v)), + rotate: (row) => (Math.atan2(row.u, row.v) * 180) / Math.PI, + stroke: '#2563eb', + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Longitude' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Latitude' } }, + }) + +export const exampleAriaLabel = 'Two-dimensional vector field' + +export const createExampleChart = () => + defineChart(vectorFieldDefinition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/42-vector-field/tanstack.ts b/benchmarks/conformance/cases/42-vector-field/tanstack.ts index 0006f759..352e831c 100644 --- a/benchmarks/conformance/cases/42-vector-field/tanstack.ts +++ b/benchmarks/conformance/cases/42-vector-field/tanstack.ts @@ -1,31 +1,11 @@ -import { defineChart, vector } from '@tanstack/charts' -import { wind } from '@charts-poc/demo-data/wind' -import { scaleLinear, scaleSqrt } from 'd3-scale' -import { sampleWind } from './selection' -import { tanstackCase } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const speed = scaleSqrt().domain([0, 14]).range([0, 22]) -const sampledWind = sampleWind(wind) +export * from './example' -export const vectorFieldDefinition = () => - defineChart({ - marks: [ - vector(sampledWind, { - id: 'wind-vectors', - x: 'longitude', - y: 'latitude', - length: (row) => speed(Math.hypot(row.u, row.v)), - rotate: (row) => (Math.atan2(row.u, row.v) * 180) / Math.PI, - stroke: '#2563eb', - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Longitude' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Latitude' } }, - }) - -export const catalogCase = tanstackCase( - vectorFieldDefinition, - 'Two-dimensional vector field', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/43-hexbin-density/example.tsx b/benchmarks/conformance/cases/43-hexbin-density/example.tsx new file mode 100644 index 00000000..29373658 --- /dev/null +++ b/benchmarks/conformance/cases/43-hexbin-density/example.tsx @@ -0,0 +1,85 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart } from '@tanstack/charts' +import { hexbin } from '@tanstack/charts/spatial/hexbin' +import { scaleLinear, scaleThreshold } from 'd3-scale' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +type CompleteCar = CarsRow & { + readonly 'economy (mpg)': number +} + +const margin = { top: 20, right: 20, bottom: 40, left: 48 } as const +const colors = ['#dbeafe', '#93c5fd', '#3b82f6', '#1d4ed8'] +const coordinate = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 1, +}) + +export const definition = (input: ExampleOptions) => { + const rows = cars + .filter((row): row is CompleteCar => row['economy (mpg)'] !== null) + .slice(input.revision * 8, input.revision * 8 + 360) + + return defineChart({ + marks: [ + hexbin(rows, { + x: 'weight (lb)', + y: 'economy (mpg)', + binWidth: 24, + color: 'count', + r: 11, + stroke: '#ffffff', + strokeWidth: 0.75, + }), + ], + x: { + scale: scaleLinear().domain([1500, 5500]), + grid: true, + axis: { label: 'Weight (lb)' }, + }, + y: { + scale: scaleLinear().domain([5, 50]), + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + color: { + scale: scaleThreshold, + domain: [5, 12, 24], + range: colors, + }, + margin, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Hexagonally binned point density' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `Bin center: ${coordinate.format(point.datum.x)} lb, ${coordinate.format(point.datum.y)} mpg · Cars: ${point.datum.count}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts b/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts index 5ea5fedb..6c029e65 100644 --- a/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts +++ b/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts @@ -1,64 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart } from '@tanstack/charts' -import { hexbin } from '@tanstack/charts/spatial/hexbin' -import { scaleLinear, scaleThreshold } from 'd3-scale' -import { tanstackCase } from '../../shared/mount' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CompleteCar = CarsRow & { - readonly 'economy (mpg)': number -} +export * from './example' -const margin = { top: 20, right: 20, bottom: 40, left: 48 } as const -const colors = ['#dbeafe', '#93c5fd', '#3b82f6', '#1d4ed8'] -const coordinate = new Intl.NumberFormat('en-US', { - maximumFractionDigits: 1, -}) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const definition = (input: ConformanceInput) => { - const rows = cars - .filter((row): row is CompleteCar => row['economy (mpg)'] !== null) - .slice(input.revision * 8, input.revision * 8 + 360) - - return defineChart({ - marks: [ - hexbin(rows, { - x: 'weight (lb)', - y: 'economy (mpg)', - binWidth: 24, - color: 'count', - r: 11, - stroke: '#ffffff', - strokeWidth: 0.75, - }), - ], - x: { - scale: scaleLinear().domain([1500, 5500]), - grid: true, - axis: { label: 'Weight (lb)' }, - }, - y: { - scale: scaleLinear().domain([5, 50]), - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - color: { - scale: scaleThreshold, - domain: [5, 12, 24], - range: colors, - }, - margin, - }) -} - -export const catalogCase = tanstackCase( - definition, - 'Hexagonally binned point density', - { - format: (point) => - `Bin center: ${coordinate.format(point.datum.x)} lb, ${coordinate.format(point.datum.y)} mpg · Cars: ${point.datum.count}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/44-framed-scatter/example.tsx b/benchmarks/conformance/cases/44-framed-scatter/example.tsx new file mode 100644 index 00000000..bf4e6830 --- /dev/null +++ b/benchmarks/conformance/cases/44-framed-scatter/example.tsx @@ -0,0 +1,64 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart, dot, frame } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' + +export const definition = (input: ExampleOptions) => { + const rows = cars + .filter((row) => row['economy (mpg)'] !== null) + .slice(input.revision * 8, input.revision * 8 + 320) + const scatterRows = rows + + return defineChart({ + marks: [ + frame({ + inset: 4, + radius: 6, + fill: '#eff6ff', + stroke: '#2563eb', + strokeOpacity: 0.7, + }), + dot(scatterRows, { + key: (row) => + `${row.name}:${row.year}:${row['weight (lb)']}:${row['economy (mpg)']}`, + x: 'weight (lb)', + y: 'economy (mpg)', + fill: '#2563eb', + fillOpacity: 0.65, + r: 3, + }), + ], + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + guides: false, + margin: 20, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = + 'Guide-free scatterplot with a framed plotting region' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/44-framed-scatter/tanstack.ts b/benchmarks/conformance/cases/44-framed-scatter/tanstack.ts index 809becdf..6c029e65 100644 --- a/benchmarks/conformance/cases/44-framed-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/44-framed-scatter/tanstack.ts @@ -1,46 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dot, frame } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => { - const rows = cars - .filter((row) => row['economy (mpg)'] !== null) - .slice(input.revision * 8, input.revision * 8 + 320) - const scatterRows = samplePreviewData(rows, input, 80, [ - (row) => row['weight (lb)'], - (row) => row['economy (mpg)'], - ]) +export * from './example' - return defineChart({ - marks: [ - frame({ - inset: 4, - radius: 6, - fill: '#eff6ff', - stroke: '#2563eb', - strokeOpacity: 0.7, - }), - dot(scatterRows, { - key: (row) => - `${row.name}:${row.year}:${row['weight (lb)']}:${row['economy (mpg)']}`, - x: 'weight (lb)', - y: 'economy (mpg)', - fill: '#2563eb', - fillOpacity: 0.65, - r: 3, - }), - ], - x: { scale: scaleLinear }, - y: { scale: scaleLinear }, - guides: false, - margin: 20, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - definition, - 'Guide-free scatterplot with a framed plotting region', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/50-empirical-cdf/example.tsx b/benchmarks/conformance/cases/50-empirical-cdf/example.tsx new file mode 100644 index 00000000..51412704 --- /dev/null +++ b/benchmarks/conformance/cases/50-empirical-cdf/example.tsx @@ -0,0 +1,83 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { d3Curve, defineChart, lineY, rank } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { curveStepAfter } from 'd3-shape' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +type CarWithEconomy = CarsRow & { 'economy (mpg)': number } + +const completeCars = cars.filter( + (row): row is CarWithEconomy => row['economy (mpg)'] !== null, +) + +const percent = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 0, +}) + +export const empiricalCdfDefinition = (input: ExampleOptions) => { + const source = completeCars + .slice(input.revision * 8) + .sort((left, right) => left['economy (mpg)'] - right['economy (mpg)']) + const ranked = rank(source, { value: 'economy (mpg)', order: 'ascending' }) + const fullRows = ranked.map((row) => ({ + ...row, + probability: row.rank / source.length, + })) + const rows = fullRows + + return defineChart({ + marks: [ + lineY(rows, { + id: 'empirical-cdf', + key: (row) => `${row.name}:${row.year}`, + x: 'economy (mpg)', + y: 'probability', + curve: d3Curve(curveStepAfter), + stroke: '#2563eb', + strokeWidth: 2, + }), + ], + x: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + y: { + scale: scaleLinear().domain([0, 1]), + grid: true, + axis: { + ticks: { format: (value) => percent.format(value) }, + label: 'Cumulative proportion', + }, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Empirical cumulative distribution' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(empiricalCdfDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.test.ts b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.test.ts index 0fd79c23..36744368 100644 --- a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.test.ts +++ b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.test.ts @@ -65,7 +65,7 @@ describe('native empirical CDF preparation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts', + 'benchmarks/conformance/cases/50-empirical-cdf/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts index e34a1078..6c029e65 100644 --- a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts +++ b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts @@ -1,66 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { d3Curve, defineChart, lineY, rank } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { curveStepAfter } from 'd3-shape' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CarWithEconomy = CarsRow & { 'economy (mpg)': number } +export * from './example' -const completeCars = cars.filter( - (row): row is CarWithEconomy => row['economy (mpg)'] !== null, -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const percent = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 0, -}) - -export const empiricalCdfDefinition = (input: ConformanceInput) => { - const source = completeCars - .slice(input.revision * 8) - .sort((left, right) => left['economy (mpg)'] - right['economy (mpg)']) - const ranked = rank(source, { value: 'economy (mpg)', order: 'ascending' }) - const fullRows = ranked.map((row) => ({ - ...row, - probability: row.rank / source.length, - })) - const rows = samplePreviewData(fullRows, input, 80, [ - (row) => row['economy (mpg)'], - (row) => row.probability, - ]) - - return defineChart({ - marks: [ - lineY(rows, { - id: 'empirical-cdf', - key: (row) => `${row.name}:${row.year}`, - x: 'economy (mpg)', - y: 'probability', - curve: d3Curve(curveStepAfter), - stroke: '#2563eb', - strokeWidth: 2, - }), - ], - x: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - y: { - scale: scaleLinear().domain([0, 1]), - grid: true, - axis: { - ticks: { format: (value) => percent.format(value) }, - label: 'Cumulative proportion', - }, - }, - }) -} - -export const mount = tanstackMount( - empiricalCdfDefinition, - 'Empirical cumulative distribution', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/51-faceted-distributions/example.tsx b/benchmarks/conformance/cases/51-faceted-distributions/example.tsx new file mode 100644 index 00000000..2d10e106 --- /dev/null +++ b/benchmarks/conformance/cases/51-faceted-distributions/example.tsx @@ -0,0 +1,119 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { binX, defineChart, facet, normalize, rect } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +export const species = ['Adelie', 'Chinstrap', 'Gentoo'] as const +export type PenguinSpecies = (typeof species)[number] +export type PenguinMass = PenguinsRow & { + readonly species: PenguinSpecies + readonly body_mass_g: number +} + +const boundaries = [2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500] +const percent = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 0, +}) + +export const facetedDistributionDefinition = (input: ExampleOptions) => { + const rows = penguins + .filter((row): row is PenguinMass => { + return ( + row.body_mass_g !== null && + species.includes(row.species as PenguinSpecies) + ) + }) + .slice(input.revision * 8, input.revision * 8 + 320) + const bins = normalize( + binX(rows, { + value: 'body_mass_g', + by: 'species', + thresholds: boundaries, + outputs: { count: { reduce: 'count' } }, + }), + { + value: 'count', + by: 'species', + basis: 'sum', + as: 'proportion', + }, + ) + .filter(({ count }) => count > 0) + .sort( + (left, right) => + species.indexOf(left.species) - species.indexOf(right.species), + ) + + return defineChart({ + marks: [ + facet(bins, { + by: 'species', + columns: 1, + gap: 8, + label: input.preview === true ? false : (group) => String(group), + chart: (facetBins) => ({ + marks: [ + rect(facetBins, { + x1: 'x1', + x2: 'x2', + y1: () => 0, + y2: 'proportion', + fill: '#8b5cf6', + inset: 0.75, + }), + ], + ...(input.preview ? { guides: false, margin: 0 } : {}), + x: { + scale: scaleLinear().domain([2500, 6500]), + grid: true, + axis: { label: 'Body mass (g)' }, + }, + y: { + scale: scaleLinear().domain([0, 0.4]), + grid: true, + axis: { + ticks: { count: 3, format: (value) => percent.format(value) }, + label: 'Proportion', + }, + }, + }), + }), + ], + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Faceted distribution comparison' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(facetedDistributionDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => + `${point.datum.species} · Body mass: ${point.datum.x1}–${point.datum.x2} g · Proportion: ${percent.format(point.datum.proportion)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.test.ts b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.test.ts index 391d1d4c..27913d6d 100644 --- a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.test.ts +++ b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.test.ts @@ -90,7 +90,7 @@ describe('definition-owned faceted distributions', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts', + 'benchmarks/conformance/cases/51-faceted-distributions/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts index b1d26725..6c029e65 100644 --- a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts +++ b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts @@ -1,98 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { binX, defineChart, facet, normalize, rect } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackCase } from '../../shared/mount' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const species = ['Adelie', 'Chinstrap', 'Gentoo'] as const -export type PenguinSpecies = (typeof species)[number] -export type PenguinMass = PenguinsRow & { - readonly species: PenguinSpecies - readonly body_mass_g: number -} +export * from './example' -const boundaries = [2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500] -const percent = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 0, -}) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const facetedDistributionDefinition = (input: ConformanceInput) => { - const rows = penguins - .filter((row): row is PenguinMass => { - return ( - row.body_mass_g !== null && - species.includes(row.species as PenguinSpecies) - ) - }) - .slice(input.revision * 8, input.revision * 8 + 320) - const bins = normalize( - binX(rows, { - value: 'body_mass_g', - by: 'species', - thresholds: boundaries, - outputs: { count: { reduce: 'count' } }, - }), - { - value: 'count', - by: 'species', - basis: 'sum', - as: 'proportion', - }, - ) - .filter(({ count }) => count > 0) - .sort( - (left, right) => - species.indexOf(left.species) - species.indexOf(right.species), - ) - - return defineChart({ - marks: [ - facet(bins, { - by: 'species', - columns: 1, - gap: 8, - label: input.preview === true ? false : (group) => String(group), - chart: (facetBins) => ({ - marks: [ - rect(facetBins, { - x1: 'x1', - x2: 'x2', - y1: () => 0, - y2: 'proportion', - fill: '#8b5cf6', - inset: 0.75, - }), - ], - ...(input.preview ? { guides: false, margin: 0 } : {}), - x: { - scale: scaleLinear().domain([2500, 6500]), - grid: true, - axis: { label: 'Body mass (g)' }, - }, - y: { - scale: scaleLinear().domain([0, 0.4]), - grid: true, - axis: { - ticks: { count: 3, format: (value) => percent.format(value) }, - label: 'Proportion', - }, - }, - }), - }), - ], - margin: 0, - }) -} - -export const catalogCase = tanstackCase( - facetedDistributionDefinition, - 'Faceted distribution comparison', - { - format: (point) => - `${point.datum.species} · Body mass: ${point.datum.x1}–${point.datum.x2} g · Proportion: ${percent.format(point.datum.proportion)}`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/52-beeswarm-dodge/example.tsx b/benchmarks/conformance/cases/52-beeswarm-dodge/example.tsx new file mode 100644 index 00000000..e151fc30 --- /dev/null +++ b/benchmarks/conformance/cases/52-beeswarm-dodge/example.tsx @@ -0,0 +1,68 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart, dodgeY, dot } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +type CarWithEconomy = CarsRow & { 'economy (mpg)': number } + +const completeCars = cars.filter( + (row): row is CarWithEconomy => row['economy (mpg)'] !== null, +) + +const margin = { + top: 20, + right: 20, + bottom: 20, + left: 20, +} + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + dot(completeCars.slice(input.revision * 8, input.revision * 8 + 72), { + x: 'economy (mpg)', + key: (row) => `${row.name}:${row.year}:${row['weight (lb)']}`, + r: 4, + fill: '#0d9488', + stroke: '#ffffff', + strokeWidth: 1, + layout: dodgeY({ + anchor: 'middle', + padding: 1, + }), + }), + ], + guides: false, + margin, + x: { + scale: scaleLinear().domain([5, 50]), + }, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Beeswarm distribution' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/52-beeswarm-dodge/tanstack.ts b/benchmarks/conformance/cases/52-beeswarm-dodge/tanstack.ts index f70e6880..6c029e65 100644 --- a/benchmarks/conformance/cases/52-beeswarm-dodge/tanstack.ts +++ b/benchmarks/conformance/cases/52-beeswarm-dodge/tanstack.ts @@ -1,44 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dodgeY, dot } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CarWithEconomy = CarsRow & { 'economy (mpg)': number } +export * from './example' -const completeCars = cars.filter( - (row): row is CarWithEconomy => row['economy (mpg)'] !== null, -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const margin = { - top: 20, - right: 20, - bottom: 20, - left: 20, -} - -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - dot(completeCars.slice(input.revision * 8, input.revision * 8 + 72), { - x: 'economy (mpg)', - key: (row) => `${row.name}:${row.year}:${row['weight (lb)']}`, - r: 4, - fill: '#0d9488', - stroke: '#ffffff', - strokeWidth: 1, - layout: dodgeY({ - anchor: 'middle', - padding: 1, - }), - }), - ], - guides: false, - margin, - x: { - scale: scaleLinear().domain([5, 50]), - }, - }) - -export const mount = tanstackMount(definition, 'Beeswarm distribution') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/53-log-scale-scatter/example.tsx b/benchmarks/conformance/cases/53-log-scale-scatter/example.tsx new file mode 100644 index 00000000..7912e0b5 --- /dev/null +++ b/benchmarks/conformance/cases/53-log-scale-scatter/example.tsx @@ -0,0 +1,72 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { flare } from '@charts-poc/demo-data/flare' +import { defineChart, dot } from '@tanstack/charts' +import { scaleLinear, scaleLog } from 'd3-scale' +import type { FlareRow } from '@charts-poc/demo-data/flare' + +type SizedFlareRow = FlareRow & { readonly size: number } + +function logScaleRows(input: ExampleOptions) { + return flare + .filter((row): row is SizedFlareRow => row.size !== null && row.size > 0) + .slice(input.revision * 8, input.revision * 8 + 200) +} + +function logScaleChart(rows: readonly SizedFlareRow[]) { + return defineChart({ + marks: [ + dot(rows, { + id: 'class-size-points', + x: 'size', + y: (row) => row.name.split('.').length - 1, + key: 'name', + r: 3.5, + fill: '#f97316', + stroke: '#9a3412', + strokeWidth: 0.75, + }), + ], + margin: { + top: 16, + right: 20, + bottom: 40, + left: 50, + }, + x: { + scale: scaleLog().domain([200, 30_000]), + grid: true, + axis: { label: 'Class size' }, + }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Hierarchy depth' } }, + }) +} + +export const logScaleScatterDefinition = (input: ExampleOptions) => + logScaleChart(logScaleRows(input)) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Flare class size on a logarithmic scale' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(logScaleScatterDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.test.ts b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.test.ts index 429defd4..0ddd2e3c 100644 --- a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.test.ts +++ b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.test.ts @@ -37,7 +37,7 @@ describe('native log-scale scatter preparation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts', + 'benchmarks/conformance/cases/53-log-scale-scatter/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts index 7805245d..6c029e65 100644 --- a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts @@ -1,68 +1,8 @@ -import { flare } from '@charts-poc/demo-data/flare' -import { defineChart, dot } from '@tanstack/charts' -import { scaleLinear, scaleLog } from 'd3-scale' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { FlareRow } from '@charts-poc/demo-data/flare' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type SizedFlareRow = FlareRow & { readonly size: number } +export * from './example' -function logScaleRows(input: ConformanceInput) { - return flare - .filter((row): row is SizedFlareRow => row.size !== null && row.size > 0) - .slice(input.revision * 8, input.revision * 8 + 200) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -function logScaleChart(rows: readonly SizedFlareRow[]) { - return defineChart({ - marks: [ - dot(rows, { - id: 'class-size-points', - x: 'size', - y: (row) => row.name.split('.').length - 1, - key: 'name', - r: 3.5, - fill: '#f97316', - stroke: '#9a3412', - strokeWidth: 0.75, - }), - ], - margin: { - top: 16, - right: 20, - bottom: 40, - left: 50, - }, - x: { - scale: scaleLog().domain([200, 30_000]), - grid: true, - axis: { label: 'Class size' }, - }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Hierarchy depth' } }, - }) -} - -export const logScaleScatterDefinition = (input: ConformanceInput) => - logScaleChart(logScaleRows(input)) - -const catalogLogScaleDefinition = (input: ConformanceInput) => { - const rows = logScaleRows(input) - return logScaleChart( - samplePreviewData(rows, input, 80, [ - (row) => row.size, - (row) => row.name.split('.').length - 1, - ]), - ) -} - -export const mount = tanstackMount( - logScaleScatterDefinition, - 'Flare class size on a logarithmic scale', -) - -export const catalogCase = tanstackCase( - catalogLogScaleDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/54-bump-ranking/example.tsx b/benchmarks/conformance/cases/54-bump-ranking/example.tsx new file mode 100644 index 00000000..f0da5322 --- /dev/null +++ b/benchmarks/conformance/cases/54-bump-ranking/example.tsx @@ -0,0 +1,122 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + d3Curve, + defineChart, + dot, + lineY, + rank, + select, + text, +} from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { curveBumpX } from 'd3-shape' +import { industries } from '@charts-poc/demo-data/industries' + +const colors = ['#2563eb', '#ea580c', '#059669', '#7c3aed', '#db2777'] +const includedIndustries = [ + 'Wholesale and Retail Trade', + 'Manufacturing', + 'Leisure and hospitality', + 'Business services', + 'Construction', +] as const +const includedIndustrySet: ReadonlySet = new Set(includedIndustries) +const observations = industries.filter( + (row) => + row.date.getUTCMonth() === 0 && + row.date.getUTCFullYear() >= 2004 && + includedIndustrySet.has(row.industry), +) + +export const bumpRankingDefinition = (input?: ExampleOptions) => { + const rows = rank(observations, { + by: 'date', + value: 'unemployed', + order: 'descending', + ties: 'competition', + }) + const labels = select(rows, { + by: 'industry', + value: (datum) => datum.date.getTime(), + select: 'max', + }) + + return defineChart({ + marks: [ + lineY(rows, { + id: 'industry-ranks', + x: 'date', + y: 'rank', + color: 'industry', + curve: d3Curve(curveBumpX), + strokeWidth: 2.25, + }), + dot(rows, { + id: 'industry-rank-points', + x: 'date', + y: 'rank', + color: 'industry', + r: 3, + }), + ...(input?.preview === true + ? [] + : [ + text(labels, { + id: 'newest-industry-labels', + x: 'date', + y: 'rank', + text: 'industry', + color: 'industry', + anchor: 'start', + dx: 6, + }), + ]), + ], + x: { + scale: scaleUtc, + axis: { + ticks: { count: 7, format: (date) => `${date.getUTCFullYear()}` }, + label: 'Year', + }, + }, + y: { + scale: scaleLinear().domain([5.2, 0.8]), + grid: true, + axis: { + ticks: { count: 5, format: (value) => `#${value}` }, + label: 'Rank', + }, + }, + color: { + domain: includedIndustries, + range: colors, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Annual unemployment rank by industry' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(bumpRankingDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/54-bump-ranking/tanstack.test.ts b/benchmarks/conformance/cases/54-bump-ranking/tanstack.test.ts index 32c81313..a1b665b0 100644 --- a/benchmarks/conformance/cases/54-bump-ranking/tanstack.test.ts +++ b/benchmarks/conformance/cases/54-bump-ranking/tanstack.test.ts @@ -51,7 +51,7 @@ describe('native bump-ranking labels', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/54-bump-ranking/tanstack.ts', + 'benchmarks/conformance/cases/54-bump-ranking/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts b/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts index 7f6c5b08..6c029e65 100644 --- a/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts +++ b/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts @@ -1,102 +1,8 @@ -import { - d3Curve, - defineChart, - dot, - lineY, - rank, - select, - text, -} from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { curveBumpX } from 'd3-shape' -import { industries } from '@charts-poc/demo-data/industries' -import type { IndustriesRow } from '@charts-poc/demo-data/industries' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#ea580c', '#059669', '#7c3aed', '#db2777'] -const includedIndustries = [ - 'Wholesale and Retail Trade', - 'Manufacturing', - 'Leisure and hospitality', - 'Business services', - 'Construction', -] as const -const includedIndustrySet: ReadonlySet = new Set(includedIndustries) -const observations = industries.filter( - (row) => - row.date.getUTCMonth() === 0 && - row.date.getUTCFullYear() >= 2004 && - includedIndustrySet.has(row.industry), -) +export * from './example' -export const bumpRankingDefinition = (input?: ConformanceInput) => { - const rows = rank(observations, { - by: 'date', - value: 'unemployed', - order: 'descending', - ties: 'competition', - }) - const labels = select(rows, { - by: 'industry', - value: (datum) => datum.date.getTime(), - select: 'max', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - lineY(rows, { - id: 'industry-ranks', - x: 'date', - y: 'rank', - color: 'industry', - curve: d3Curve(curveBumpX), - strokeWidth: 2.25, - }), - dot(rows, { - id: 'industry-rank-points', - x: 'date', - y: 'rank', - color: 'industry', - r: 3, - }), - ...(input?.preview === true - ? [] - : [ - text(labels, { - id: 'newest-industry-labels', - x: 'date', - y: 'rank', - text: 'industry', - color: 'industry', - anchor: 'start', - dx: 6, - }), - ]), - ], - x: { - scale: scaleUtc, - axis: { - ticks: { count: 7, format: (date) => `${date.getUTCFullYear()}` }, - label: 'Year', - }, - }, - y: { - scale: scaleLinear().domain([5.2, 0.8]), - grid: true, - axis: { - ticks: { count: 5, format: (value) => `#${value}` }, - label: 'Rank', - }, - }, - color: { - domain: includedIndustries, - range: colors, - }, - }) -} - -export const mount = tanstackMount( - bumpRankingDefinition, - 'Annual unemployment rank by industry', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/55-indexed-multi-line/example.tsx b/benchmarks/conformance/cases/55-indexed-multi-line/example.tsx new file mode 100644 index 00000000..fbbc2ddc --- /dev/null +++ b/benchmarks/conformance/cases/55-indexed-multi-line/example.tsx @@ -0,0 +1,126 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + defineChart, + lineY, + normalize, + ruleY, + select, + text, +} from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' +import type { IndustriesRow } from '@charts-poc/demo-data/industries' + +const colors = ['#2563eb', '#ea580c', '#059669', '#7c3aed'] +const formatIndex = (value: number) => `${Math.round((value - 1) * 100)}%` +const includedIndustries = new Set([ + 'Construction', + 'Finance', + 'Government', + 'Manufacturing', +]) +export const indexedIndustryObservations = industries.filter( + (row) => + row.date >= new Date(Date.UTC(2008, 0, 1)) && + includedIndustries.has(row.industry), +) + +export const indexedIndustryDefinition = ( + source: readonly IndustriesRow[], + preview = false, +) => { + const rows = normalize( + [...source].sort( + (left, right) => left.date.getTime() - right.date.getTime(), + ), + { + value: 'unemployed', + by: 'industry', + basis: 'first', + as: 'indexed', + }, + ) + const labels = select(rows, { + by: 'industry', + value: (datum) => datum.date.getTime(), + select: 'max', + }) + return defineChart({ + marks: [ + ruleY([1], { strokeOpacity: 0.65 }), + lineY(rows, { + id: 'indexed-lines', + x: 'date', + y: 'indexed', + color: 'industry', + strokeWidth: 2.25, + }), + ...(preview + ? [] + : [ + text(labels, { + id: 'end-labels', + x: 'date', + y: 'indexed', + text: 'industry', + color: 'industry', + anchor: 'start', + dx: 5, + }), + ]), + ], + x: { scale: scaleUtc, axis: { label: 'Month' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { + ticks: { format: formatIndex }, + label: 'Change from January 2008', + }, + }, + color: { + range: colors, + }, + margin: { right: 108 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const createExampleDefinition = (input: ExampleOptions) => + indexedIndustryDefinition(indexedIndustryObservations, input.preview === true) + +export const exampleAriaLabel = 'Indexed U.S. industry unemployment' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(createExampleDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + })} · ${formatIndex(datum.indexed)} from start`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.test.ts b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.test.ts index 65c49edb..bda96feb 100644 --- a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.test.ts +++ b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.test.ts @@ -85,7 +85,7 @@ describe('definition-owned indexed industry lines', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts', + 'benchmarks/conformance/cases/55-indexed-multi-line/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts index 9f1fb005..6c029e65 100644 --- a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts +++ b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts @@ -1,104 +1,8 @@ -import { - defineChart, - lineY, - normalize, - ruleY, - select, - text, -} from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import type { IndustriesRow } from '@charts-poc/demo-data/industries' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#ea580c', '#059669', '#7c3aed'] -const formatIndex = (value: number) => `${Math.round((value - 1) * 100)}%` -const includedIndustries = new Set([ - 'Construction', - 'Finance', - 'Government', - 'Manufacturing', -]) -export const indexedIndustryObservations = industries.filter( - (row) => - row.date >= new Date(Date.UTC(2008, 0, 1)) && - includedIndustries.has(row.industry), -) +export * from './example' -export const indexedIndustryDefinition = ( - source: readonly IndustriesRow[], - preview = false, -) => { - const rows = normalize( - [...source].sort( - (left, right) => left.date.getTime() - right.date.getTime(), - ), - { - value: 'unemployed', - by: 'industry', - basis: 'first', - as: 'indexed', - }, - ) - const labels = select(rows, { - by: 'industry', - value: (datum) => datum.date.getTime(), - select: 'max', - }) - return defineChart({ - marks: [ - ruleY([1], { strokeOpacity: 0.65 }), - lineY(rows, { - id: 'indexed-lines', - x: 'date', - y: 'indexed', - color: 'industry', - strokeWidth: 2.25, - }), - ...(preview - ? [] - : [ - text(labels, { - id: 'end-labels', - x: 'date', - y: 'indexed', - text: 'industry', - color: 'industry', - anchor: 'start', - dx: 5, - }), - ]), - ], - x: { scale: scaleUtc, axis: { label: 'Month' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { - ticks: { format: formatIndex }, - label: 'Change from January 2008', - }, - }, - color: { - range: colors, - }, - margin: { right: 108 }, - }) -} +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - (input: ConformanceInput) => - indexedIndustryDefinition( - indexedIndustryObservations, - input.preview === true, - ), - 'Indexed U.S. industry unemployment', - { - format: ({ datum }) => - `${datum.industry} · ${datum.date.toLocaleDateString('en-US', { - month: 'short', - year: 'numeric', - timeZone: 'UTC', - })} · ${formatIndex(datum.indexed)} from start`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/56-connected-scatter/example.tsx b/benchmarks/conformance/cases/56-connected-scatter/example.tsx new file mode 100644 index 00000000..08e03b8d --- /dev/null +++ b/benchmarks/conformance/cases/56-connected-scatter/example.tsx @@ -0,0 +1,108 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + arrow, + d3Curve, + defineChart, + dot, + lineY, + text, + rollingWindow, +} from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { curveCatmullRom } from 'd3-shape' +import { driving } from '@charts-poc/demo-data/driving' + +export const directionTargetIndexes = [14, 28, 42] as const +const directionTargetIndexSet: ReadonlySet = new Set( + directionTargetIndexes, +) +export const directionPairs = rollingWindow(driving, { + orderBy: 'year', + size: 2, + partial: false, + outputs: {}, +}).filter(({ sourceIndexes }) => + directionTargetIndexSet.has(sourceIndexes[1] ?? -1), +) +const labels = driving.filter((row) => row.year % 5 === 0) + +export const connectedScatterDefinition = (input?: ExampleOptions) => + defineChart({ + marks: [ + lineY(driving, { + id: 'driving-path', + x: 'miles', + y: 'gas', + stroke: '#64748b', + strokeWidth: 2.25, + curve: d3Curve(curveCatmullRom.alpha(0.5)), + }), + dot(driving, { + id: 'driving-points', + x: 'miles', + y: 'gas', + fill: '#0f766e', + r: 3.25, + }), + arrow(directionPairs, { + id: 'direction-arrows', + x1: ({ source }) => source[0]?.miles, + y1: ({ source }) => source[0]?.gas, + x2: 'miles', + y2: 'gas', + stroke: '#0f766e', + strokeWidth: 1.5, + headLength: 7, + }), + ...(input?.preview === true + ? [] + : [ + text(labels, { + id: 'year-labels', + x: 'miles', + y: 'gas', + text: (row) => `${row.year}`, + fill: '#0f172a', + anchor: 'middle', + dy: -9, + }), + ]), + ], + x: { + scale: scaleLinear, + grid: true, + axis: { label: 'Miles driven per person' }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Cost of gasoline ($ per gallon)' }, + }, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Directed connected scatterplot over time' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(connectedScatterDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/56-connected-scatter/tanstack.test.ts b/benchmarks/conformance/cases/56-connected-scatter/tanstack.test.ts index 618e21d6..6c61e6c3 100644 --- a/benchmarks/conformance/cases/56-connected-scatter/tanstack.test.ts +++ b/benchmarks/conformance/cases/56-connected-scatter/tanstack.test.ts @@ -64,7 +64,7 @@ describe('definition-owned connected-scatter direction', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/56-connected-scatter/tanstack.ts', + 'benchmarks/conformance/cases/56-connected-scatter/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts b/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts index c7572483..6c029e65 100644 --- a/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts @@ -1,87 +1,8 @@ -import { - arrow, - d3Curve, - defineChart, - dot, - lineY, - text, - rollingWindow, -} from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { curveCatmullRom } from 'd3-shape' -import { driving } from '@charts-poc/demo-data/driving' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const directionTargetIndexes = [14, 28, 42] as const -const directionTargetIndexSet: ReadonlySet = new Set( - directionTargetIndexes, -) -export const directionPairs = rollingWindow(driving, { - orderBy: 'year', - size: 2, - partial: false, - outputs: {}, -}).filter(({ sourceIndexes }) => - directionTargetIndexSet.has(sourceIndexes[1] ?? -1), -) -const labels = driving.filter((row) => row.year % 5 === 0) +export * from './example' -export const connectedScatterDefinition = (input?: ConformanceInput) => - defineChart({ - marks: [ - lineY(driving, { - id: 'driving-path', - x: 'miles', - y: 'gas', - stroke: '#64748b', - strokeWidth: 2.25, - curve: d3Curve(curveCatmullRom.alpha(0.5)), - }), - dot(driving, { - id: 'driving-points', - x: 'miles', - y: 'gas', - fill: '#0f766e', - r: 3.25, - }), - arrow(directionPairs, { - id: 'direction-arrows', - x1: ({ source }) => source[0]?.miles, - y1: ({ source }) => source[0]?.gas, - x2: 'miles', - y2: 'gas', - stroke: '#0f766e', - strokeWidth: 1.5, - headLength: 7, - }), - ...(input?.preview === true - ? [] - : [ - text(labels, { - id: 'year-labels', - x: 'miles', - y: 'gas', - text: (row) => `${row.year}`, - fill: '#0f172a', - anchor: 'middle', - dy: -9, - }), - ]), - ], - x: { - scale: scaleLinear, - grid: true, - axis: { label: 'Miles driven per person' }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Cost of gasoline ($ per gallon)' }, - }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const mount = tanstackMount( - connectedScatterDefinition, - 'Directed connected scatterplot over time', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx b/benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx new file mode 100644 index 00000000..c9f6f736 --- /dev/null +++ b/benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx @@ -0,0 +1,219 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { + binX, + binY, + colorLegend, + defineChart, + dot, + rect, +} from '@tanstack/charts' +import { viewGrid } from '@tanstack/charts/view' +import { scaleLinear } from 'd3-scale' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +export type CompletePenguin = PenguinsRow & { + readonly flipper_length_mm: number + readonly body_mass_g: number +} + +export const flipperBoundaries = [170, 180, 190, 200, 210, 220, 230, 240] +export const massBoundaries = [ + 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, +] + +const colors = ['#2563eb', '#ea580c', '#059669'] + +function scatterRows(input: ExampleOptions) { + return penguins + .filter((row): row is CompletePenguin => { + return row.flipper_length_mm !== null && row.body_mass_g !== null + }) + .slice(input.revision * 8, input.revision * 8 + 320) +} + +function scatterMarginalChart( + rows: readonly CompletePenguin[], + scatter: readonly CompletePenguin[], + showLegend: boolean, + preview: boolean, +) { + const xBins = binX(rows, { + value: 'flipper_length_mm', + thresholds: flipperBoundaries, + outputs: { count: { reduce: 'count' } }, + }) + const yBins = binY(rows, { + value: 'body_mass_g', + thresholds: massBoundaries, + outputs: { count: { reduce: 'count' } }, + }) + const flipperScale = scaleLinear().domain([ + flipperBoundaries[0]!, + flipperBoundaries.at(-1)!, + ]) + const massScale = scaleLinear().domain([ + massBoundaries[0]!, + massBoundaries.at(-1)!, + ]) + + return viewGrid({ + id: 'penguin-marginals', + rows: [ + { id: 'top', size: preview ? 48 : 82 }, + { id: 'main', grow: 1 }, + ], + columns: [ + { id: 'main', grow: 1 }, + { id: 'right', size: preview ? 48 : 82 }, + ], + gap: preview ? 4 : 8, + views: [ + { + id: 'main', + row: 'main', + column: 'main', + chart: defineChart({ + marks: [ + dot(scatter, { + id: 'penguins', + x: 'flipper_length_mm', + y: 'body_mass_g', + color: 'species', + key: (row) => + JSON.stringify([ + row.species, + row.island, + row.culmen_length_mm, + row.culmen_depth_mm, + row.flipper_length_mm, + row.body_mass_g, + row.sex, + ]), + r: 3, + fillOpacity: 0.78, + }), + ], + x: { + scale: flipperScale, + grid: true, + axis: { label: 'Flipper length (mm)' }, + }, + y: { + scale: massScale, + grid: true, + axis: { label: 'Body mass (g)' }, + }, + color: { + range: colors, + ...(showLegend + ? { legend: colorLegend({ label: 'Species' }) } + : {}), + }, + ...(preview ? { guides: false, margin: 0 } : {}), + }), + }, + { + id: 'top', + row: 'top', + column: 'main', + share: { x: 'main' }, + chart: defineChart({ + marks: [ + rect(xBins, { + id: 'flipper-histogram', + x: 'x', + x1: 'x1', + x2: 'x2', + y: 'count', + y1: () => 0, + y2: 'count', + fill: '#0ea5e9', + fillOpacity: 0.78, + inset: 1, + }), + ], + x: { scale: flipperScale }, + y: { scale: scaleLinear }, + guides: false, + }), + }, + { + id: 'right', + row: 'main', + column: 'right', + share: { y: 'main' }, + chart: defineChart({ + marks: [ + rect(yBins, { + id: 'mass-histogram', + x: 'count', + x1: () => 0, + x2: 'count', + y: 'y', + y1: 'y1', + y2: 'y2', + fill: '#f97316', + fillOpacity: 0.78, + inset: 1, + }), + ], + x: { scale: scaleLinear }, + y: { scale: massScale }, + guides: false, + }), + }, + ], + }) +} + +export const scatterMarginalDefinition = (input: ExampleOptions) => { + const rows = scatterRows(input) + return scatterMarginalChart( + rows, + rows, + input.preview !== true, + input.preview === true, + ) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Scatterplot with marginal histograms' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(scatterMarginalDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: (point) => { + const datum = point.datum + if ('source' in datum && 'x1' in datum) { + return `Flipper length: ${datum.x1}–${datum.x2} mm · ${datum.count} penguins` + } + if ('source' in datum && 'y1' in datum) { + return `Body mass: ${datum.y1}–${datum.y2} g · ${datum.count} penguins` + } + return `${datum.species} · ${datum.flipper_length_mm} mm · ${datum.body_mass_g} g` + }, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.test.ts b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.test.ts index 867aca4e..6a39cc8a 100644 --- a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.test.ts +++ b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.test.ts @@ -136,7 +136,7 @@ describe('scatterplot with marginal histograms', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts', + 'benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts index 417ba47e..6c029e65 100644 --- a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts +++ b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts @@ -1,211 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { - binX, - binY, - colorLegend, - defineChart, - dot, - rect, -} from '@tanstack/charts' -import { viewGrid } from '@tanstack/charts/view' -import { scaleLinear } from 'd3-scale' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export type CompletePenguin = PenguinsRow & { - readonly flipper_length_mm: number - readonly body_mass_g: number -} +export * from './example' -export const flipperBoundaries = [170, 180, 190, 200, 210, 220, 230, 240] -export const massBoundaries = [ - 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, -] +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const colors = ['#2563eb', '#ea580c', '#059669'] - -function scatterRows(input: ConformanceInput) { - return penguins - .filter((row): row is CompletePenguin => { - return row.flipper_length_mm !== null && row.body_mass_g !== null - }) - .slice(input.revision * 8, input.revision * 8 + 320) -} - -function scatterMarginalChart( - rows: readonly CompletePenguin[], - scatter: readonly CompletePenguin[], - showLegend: boolean, - preview: boolean, -) { - const xBins = binX(rows, { - value: 'flipper_length_mm', - thresholds: flipperBoundaries, - outputs: { count: { reduce: 'count' } }, - }) - const yBins = binY(rows, { - value: 'body_mass_g', - thresholds: massBoundaries, - outputs: { count: { reduce: 'count' } }, - }) - const flipperScale = scaleLinear().domain([ - flipperBoundaries[0]!, - flipperBoundaries.at(-1)!, - ]) - const massScale = scaleLinear().domain([ - massBoundaries[0]!, - massBoundaries.at(-1)!, - ]) - - return viewGrid({ - id: 'penguin-marginals', - rows: [ - { id: 'top', size: preview ? 48 : 82 }, - { id: 'main', grow: 1 }, - ], - columns: [ - { id: 'main', grow: 1 }, - { id: 'right', size: preview ? 48 : 82 }, - ], - gap: preview ? 4 : 8, - views: [ - { - id: 'main', - row: 'main', - column: 'main', - chart: defineChart({ - marks: [ - dot(scatter, { - id: 'penguins', - x: 'flipper_length_mm', - y: 'body_mass_g', - color: 'species', - key: (row) => - JSON.stringify([ - row.species, - row.island, - row.culmen_length_mm, - row.culmen_depth_mm, - row.flipper_length_mm, - row.body_mass_g, - row.sex, - ]), - r: 3, - fillOpacity: 0.78, - }), - ], - x: { - scale: flipperScale, - grid: true, - axis: { label: 'Flipper length (mm)' }, - }, - y: { - scale: massScale, - grid: true, - axis: { label: 'Body mass (g)' }, - }, - color: { - range: colors, - ...(showLegend - ? { legend: colorLegend({ label: 'Species' }) } - : {}), - }, - ...(preview ? { guides: false, margin: 0 } : {}), - }), - }, - { - id: 'top', - row: 'top', - column: 'main', - share: { x: 'main' }, - chart: defineChart({ - marks: [ - rect(xBins, { - id: 'flipper-histogram', - x: 'x', - x1: 'x1', - x2: 'x2', - y: 'count', - y1: () => 0, - y2: 'count', - fill: '#0ea5e9', - fillOpacity: 0.78, - inset: 1, - }), - ], - x: { scale: flipperScale }, - y: { scale: scaleLinear }, - guides: false, - }), - }, - { - id: 'right', - row: 'main', - column: 'right', - share: { y: 'main' }, - chart: defineChart({ - marks: [ - rect(yBins, { - id: 'mass-histogram', - x: 'count', - x1: () => 0, - x2: 'count', - y: 'y', - y1: 'y1', - y2: 'y2', - fill: '#f97316', - fillOpacity: 0.78, - inset: 1, - }), - ], - x: { scale: scaleLinear }, - y: { scale: massScale }, - guides: false, - }), - }, - ], - }) -} - -export const scatterMarginalDefinition = (input: ConformanceInput) => { - const rows = scatterRows(input) - return scatterMarginalChart(rows, rows, true, false) -} - -const catalogScatterMarginalDefinition = (input: ConformanceInput) => { - const rows = scatterRows(input) - return scatterMarginalChart( - rows, - samplePreviewData(rows, input, 80, [ - (row) => row.flipper_length_mm, - (row) => row.body_mass_g, - ]), - input.preview !== true, - input.preview === true, - ) -} - -export const mount = tanstackMount( - scatterMarginalDefinition, - 'Scatterplot with marginal histograms', - { - format: (point) => { - const datum = point.datum - if ('source' in datum && 'x1' in datum) { - return `Flipper length: ${datum.x1}–${datum.x2} mm · ${datum.count} penguins` - } - if ('source' in datum && 'y1' in datum) { - return `Body mass: ${datum.y1}–${datum.y2} g · ${datum.count} penguins` - } - return `${datum.species} · ${datum.flipper_length_mm} mm · ${datum.body_mass_g} g` - }, - }, -) - -export const catalogCase = tanstackCase( - catalogScatterMarginalDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/58-select-extrema/example.tsx b/benchmarks/conformance/cases/58-select-extrema/example.tsx new file mode 100644 index 00000000..6400bcfc --- /dev/null +++ b/benchmarks/conformance/cases/58-select-extrema/example.tsx @@ -0,0 +1,102 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, dot, lineY, select, text } from '@tanstack/charts' +import { decorative } from '@tanstack/charts/mark/decorative' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { aapl } from '@charts-poc/demo-data/aapl' +import type { AaplRow } from '@charts-poc/demo-data/aapl' + +const annotationColor = '#dc2626' +const dateKey = (row: AaplRow) => row.Date.getTime() + +export const minimumAapl = select(aapl, { + value: 'Close', + select: 'min', +}) +export const maximumAapl = select(aapl, { + value: 'Close', + select: 'max', +}) + +function selectExtremaChart(rows: readonly AaplRow[], preview = false) { + return defineChart({ + marks: [ + lineY(rows, { + id: 'close-line', + x: 'Date', + y: 'Close', + key: dateKey, + stroke: '#2563eb', + strokeWidth: 2.25, + }), + dot(minimumAapl, { + id: 'minimum-point', + x: 'Date', + y: 'Close', + key: dateKey, + fill: annotationColor, + r: 5, + }), + dot(maximumAapl, { + id: 'maximum-point', + x: 'Date', + y: 'Close', + key: dateKey, + fill: annotationColor, + r: 5, + }), + decorative( + text(minimumAapl, { + id: 'minimum-label', + x: 'Date', + y: 'Close', + key: dateKey, + text: ({ Close }) => `Low $${Close.toFixed(2)}`, + fill: annotationColor, + anchor: preview ? 'start' : 'middle', + dx: preview ? 6 : 0, + dy: preview ? -13 : 13, + }), + ), + decorative( + text(maximumAapl, { + id: 'maximum-label', + x: 'Date', + y: 'Close', + key: dateKey, + text: ({ Close }) => `High $${Close.toFixed(2)}`, + fill: annotationColor, + anchor: 'end', + dx: -7, + dy: preview ? 13 : -13, + }), + ), + ], + x: { scale: scaleUtc, axis: { label: 'Date' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, + }) +} + +export interface ExampleOptions { + compact?: boolean +} + +export const selectExtremaDefinition = ({ + compact = false, +}: ExampleOptions = {}) => selectExtremaChart(aapl, compact) + +export const exampleAriaLabel = + 'Apple closing price with minimum and maximum annotations' + +export const createExampleChart = (options: ExampleOptions = {}) => + defineChart(selectExtremaDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/58-select-extrema/tanstack.test.ts b/benchmarks/conformance/cases/58-select-extrema/tanstack.test.ts index 05919f7c..d9ae0689 100644 --- a/benchmarks/conformance/cases/58-select-extrema/tanstack.test.ts +++ b/benchmarks/conformance/cases/58-select-extrema/tanstack.test.ts @@ -67,7 +67,7 @@ describe('definition-owned extrema annotations', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/58-select-extrema/tanstack.ts', + 'benchmarks/conformance/cases/58-select-extrema/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/58-select-extrema/tanstack.ts b/benchmarks/conformance/cases/58-select-extrema/tanstack.ts index 9897892b..57f3320f 100644 --- a/benchmarks/conformance/cases/58-select-extrema/tanstack.ts +++ b/benchmarks/conformance/cases/58-select-extrema/tanstack.ts @@ -1,101 +1,14 @@ -import { defineChart, dot, lineY, select, text } from '@tanstack/charts' -import { decorative } from '@tanstack/charts/mark/decorative' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { aapl } from '@charts-poc/demo-data/aapl' -import type { AaplRow } from '@charts-poc/demo-data/aapl' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const annotationColor = '#dc2626' -const dateKey = (row: AaplRow) => row.Date.getTime() +export * from './example' -export const minimumAapl = select(aapl, { - value: 'Close', - select: 'min', -}) -export const maximumAapl = select(aapl, { - value: 'Close', - select: 'max', -}) - -function selectExtremaChart(rows: readonly AaplRow[], preview = false) { - return defineChart({ - marks: [ - lineY(rows, { - id: 'close-line', - x: 'Date', - y: 'Close', - key: dateKey, - stroke: '#2563eb', - strokeWidth: 2.25, - }), - dot(minimumAapl, { - id: 'minimum-point', - x: 'Date', - y: 'Close', - key: dateKey, - fill: annotationColor, - r: 5, - }), - dot(maximumAapl, { - id: 'maximum-point', - x: 'Date', - y: 'Close', - key: dateKey, - fill: annotationColor, - r: 5, - }), - decorative( - text(minimumAapl, { - id: 'minimum-label', - x: 'Date', - y: 'Close', - key: dateKey, - text: ({ Close }) => `Low $${Close.toFixed(2)}`, - fill: annotationColor, - anchor: preview ? 'start' : 'middle', - dx: preview ? 6 : 0, - dy: preview ? -13 : 13, - }), - ), - decorative( - text(maximumAapl, { - id: 'maximum-label', - x: 'Date', - y: 'Close', - key: dateKey, - text: ({ Close }) => `High $${Close.toFixed(2)}`, - fill: annotationColor, - anchor: 'end', - dx: -7, - dy: preview ? 13 : -13, - }), - ), - ], - x: { scale: scaleUtc, axis: { label: 'Date' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, - }) -} - -export const selectExtremaDefinition = () => selectExtremaChart(aapl) - -const catalogSelectExtremaDefinition = (input: ConformanceInput) => - selectExtremaChart( - samplePreviewData(aapl, input, 80, [ - (row) => row.Date.getTime(), - (row) => row.Close, - ]), - input.preview === true, - ) - -export const mount = tanstackMount( - selectExtremaDefinition, - 'Apple closing price with minimum and maximum annotations', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const catalogCase = tanstackCase( - catalogSelectExtremaDefinition, - mount.ariaLabel, - mount.interactiveTooltip, +export const catalogCase = tanstackExampleMount( + (input) => createExampleChart({ compact: input.preview }), + exampleAriaLabel, ) diff --git a/benchmarks/conformance/cases/59-grouped-reducer-bars/example.tsx b/benchmarks/conformance/cases/59-grouped-reducer-bars/example.tsx new file mode 100644 index 00000000..b0ad2586 --- /dev/null +++ b/benchmarks/conformance/cases/59-grouped-reducer-bars/example.tsx @@ -0,0 +1,75 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { barY, defineChart, groupBy, text } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import { penguins } from '@charts-poc/demo-data/penguins' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +type PenguinWithMass = PenguinsRow & { body_mass_g: number } + +const observations = penguins.filter( + (row): row is PenguinWithMass => row.body_mass_g !== null, +) +const formatMass = (value: number) => + value.toLocaleString('en-US', { maximumFractionDigits: 3 }) + +export const definition = (input?: ExampleOptions) => { + const rows = groupBy(observations, { + by: 'species', + outputs: { + meanBodyMass: { value: 'body_mass_g', reduce: 'mean' }, + }, + }) + + return defineChart({ + marks: [ + barY(rows, { + x: 'species', + y: 'meanBodyMass', + fill: '#0ea5e9', + inset: 1, + }), + text(rows, { + x: 'species', + y: 'meanBodyMass', + text: (row) => formatMass(row.meanBodyMass), + fill: '#0c4a6e', + dy: input?.preview === true ? 10 : -8, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Mean body mass (g)' }, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Mean penguin body mass by species' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts b/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts index 79846021..6c029e65 100644 --- a/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts +++ b/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts @@ -1,56 +1,8 @@ -import { barY, defineChart, groupBy, text } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { penguins } from '@charts-poc/demo-data/penguins' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type PenguinWithMass = PenguinsRow & { body_mass_g: number } +export * from './example' -const observations = penguins.filter( - (row): row is PenguinWithMass => row.body_mass_g !== null, -) -const formatMass = (value: number) => - value.toLocaleString('en-US', { maximumFractionDigits: 3 }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const definition = (input?: ConformanceInput) => { - const rows = groupBy(observations, { - by: 'species', - outputs: { - meanBodyMass: { value: 'body_mass_g', reduce: 'mean' }, - }, - }) - - return defineChart({ - marks: [ - barY(rows, { - x: 'species', - y: 'meanBodyMass', - fill: '#0ea5e9', - inset: 1, - }), - text(rows, { - x: 'species', - y: 'meanBodyMass', - text: (row) => formatMass(row.meanBodyMass), - fill: '#0c4a6e', - dy: input?.preview === true ? 10 : -8, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Mean body mass (g)' }, - }, - }) -} - -export const catalogCase = tanstackCase( - definition, - 'Mean penguin body mass by species', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/60-lag-autocorrelation/example.tsx b/benchmarks/conformance/cases/60-lag-autocorrelation/example.tsx new file mode 100644 index 00000000..d1e197c3 --- /dev/null +++ b/benchmarks/conformance/cases/60-lag-autocorrelation/example.tsx @@ -0,0 +1,70 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, dot, first, lineY, rollingWindow } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import { aapl } from '@charts-poc/demo-data/aapl' + +interface ReferencePoint { + PreviousClose: number + Close: number +} + +const observations = aapl.slice(-120) +const closeDomain: readonly [number, number] = [150, 195] +const identity: readonly ReferencePoint[] = [ + { PreviousClose: closeDomain[0], Close: closeDomain[0] }, + { PreviousClose: closeDomain[1], Close: closeDomain[1] }, +] + +export const lagAutocorrelationDefinition = () => { + const rows = rollingWindow(observations, { + size: 2, + orderBy: 'Date', + partial: false, + outputs: { PreviousClose: { value: 'Close', reduce: first } }, + }) + + return defineChart({ + marks: [ + lineY(identity, { + x: 'PreviousClose', + y: 'Close', + stroke: '#94a3b8', + strokeDasharray: '5,4', + }), + dot(rows, { + x: 'PreviousClose', + y: 'Close', + fill: '#7c3aed', + fillOpacity: 0.78, + r: 4, + }), + ], + x: { + scale: scaleLinear().domain(closeDomain), + grid: true, + axis: { label: 'Previous close (USD)' }, + }, + y: { + scale: scaleLinear().domain(closeDomain), + grid: true, + axis: { label: 'Current close (USD)' }, + }, + }) +} + +export const exampleAriaLabel = + 'Lag-one autocorrelation of Apple closing prices' + +export const createExampleChart = () => + defineChart(lagAutocorrelationDefinition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts b/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts index 27bfba2a..352e831c 100644 --- a/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts +++ b/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts @@ -1,58 +1,11 @@ -import { defineChart, dot, first, lineY, rollingWindow } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { aapl } from '@charts-poc/demo-data/aapl' -import { tanstackMount } from '../../shared/mount' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -interface ReferencePoint { - PreviousClose: number - Close: number -} +export * from './example' -const observations = aapl.slice(-120) -const closeDomain: readonly [number, number] = [150, 195] -const identity: readonly ReferencePoint[] = [ - { PreviousClose: closeDomain[0], Close: closeDomain[0] }, - { PreviousClose: closeDomain[1], Close: closeDomain[1] }, -] - -export const lagAutocorrelationDefinition = () => { - const rows = rollingWindow(observations, { - size: 2, - orderBy: 'Date', - partial: false, - outputs: { PreviousClose: { value: 'Close', reduce: first } }, - }) - - return defineChart({ - marks: [ - lineY(identity, { - x: 'PreviousClose', - y: 'Close', - stroke: '#94a3b8', - strokeDasharray: '5,4', - }), - dot(rows, { - x: 'PreviousClose', - y: 'Close', - fill: '#7c3aed', - fillOpacity: 0.78, - r: 4, - }), - ], - x: { - scale: scaleLinear().domain(closeDomain), - grid: true, - axis: { label: 'Previous close (USD)' }, - }, - y: { - scale: scaleLinear().domain(closeDomain), - grid: true, - axis: { label: 'Current close (USD)' }, - }, - }) -} - -export const mount = tanstackMount( - lagAutocorrelationDefinition, - 'Lag-one autocorrelation of Apple closing prices', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/61-quantile-ribbon/example.tsx b/benchmarks/conformance/cases/61-quantile-ribbon/example.tsx new file mode 100644 index 00000000..ac212f73 --- /dev/null +++ b/benchmarks/conformance/cases/61-quantile-ribbon/example.tsx @@ -0,0 +1,72 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { areaY, defineChart, groupBy, lineY, quantile } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { industries } from '@charts-poc/demo-data/industries' + +export const quantileRows = groupBy(industries, { + by: 'date', + outputs: { + lower: { value: 'unemployed', reduce: quantile(0.1) }, + median: { value: 'unemployed', reduce: quantile(0.5) }, + upper: { value: 'unemployed', reduce: quantile(0.9) }, + }, +}) + +const dateKey = ({ date }: (typeof quantileRows)[number]) => date.getTime() + +function quantileRibbonChart( + rows: readonly (typeof quantileRows)[number][], + showAxisLabels: boolean, +) { + return defineChart({ + marks: [ + areaY(rows, { + id: 'quantile-ribbon', + x: 'date', + y1: 'lower', + y2: 'upper', + key: dateKey, + fill: '#0ea5e9', + fillOpacity: 0.22, + }), + lineY(rows, { + id: 'median-line', + x: 'date', + y: 'median', + key: dateKey, + stroke: '#0369a1', + strokeWidth: 2.25, + }), + ], + x: { + scale: scaleUtc, + axis: showAxisLabels ? { label: 'Month' } : {}, + }, + y: { + scale: scaleLinear, + grid: true, + axis: showAxisLabels + ? { label: 'Unemployed people by industry (thousands)' } + : {}, + }, + }) +} + +export const quantileRibbonDefinition = () => + quantileRibbonChart(quantileRows, true) + +export const exampleAriaLabel = 'Monthly industry unemployment distribution' + +export const createExampleChart = () => + defineChart(quantileRibbonDefinition(), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart() + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.test.ts b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.test.ts index d86e6b7c..71961722 100644 --- a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.test.ts +++ b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.test.ts @@ -94,7 +94,7 @@ describe('definition-owned quantile ribbon', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts', + 'benchmarks/conformance/cases/61-quantile-ribbon/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts index 2f04f7d1..352e831c 100644 --- a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts +++ b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts @@ -1,79 +1,11 @@ -import { areaY, defineChart, groupBy, lineY, quantile } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { industries } from '@charts-poc/demo-data/industries' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -export const quantileRows = groupBy(industries, { - by: 'date', - outputs: { - lower: { value: 'unemployed', reduce: quantile(0.1) }, - median: { value: 'unemployed', reduce: quantile(0.5) }, - upper: { value: 'unemployed', reduce: quantile(0.9) }, - }, -}) +export * from './example' -const dateKey = ({ date }: (typeof quantileRows)[number]) => date.getTime() - -function quantileRibbonChart( - rows: readonly (typeof quantileRows)[number][], - showAxisLabels: boolean, -) { - return defineChart({ - marks: [ - areaY(rows, { - id: 'quantile-ribbon', - x: 'date', - y1: 'lower', - y2: 'upper', - key: dateKey, - fill: '#0ea5e9', - fillOpacity: 0.22, - }), - lineY(rows, { - id: 'median-line', - x: 'date', - y: 'median', - key: dateKey, - stroke: '#0369a1', - strokeWidth: 2.25, - }), - ], - x: { - scale: scaleUtc, - axis: showAxisLabels ? { label: 'Month' } : {}, - }, - y: { - scale: scaleLinear, - grid: true, - axis: showAxisLabels - ? { label: 'Unemployed people by industry (thousands)' } - : {}, - }, - }) -} - -export const quantileRibbonDefinition = () => - quantileRibbonChart(quantileRows, true) - -const catalogQuantileRibbonDefinition = (input: ConformanceInput) => - quantileRibbonChart( - samplePreviewData(quantileRows, input, 64, [ - (row) => row.date.getTime(), - (row) => row.lower, - (row) => row.upper, - ]), - input.preview !== true, - ) - -export const mount = tanstackMount( - quantileRibbonDefinition, - 'Monthly industry unemployment distribution', +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, ) -export const catalogCase = tanstackCase( - catalogQuantileRibbonDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/62-ridgeline-density/example.tsx b/benchmarks/conformance/cases/62-ridgeline-density/example.tsx new file mode 100644 index 00000000..762b9199 --- /dev/null +++ b/benchmarks/conformance/cases/62-ridgeline-density/example.tsx @@ -0,0 +1,105 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { simpsons } from '@charts-poc/demo-data/simpsons' +import { + binX, + d3Curve, + defineChart, + normalize, + ridgelineY, + ruleY, +} from '@tanstack/charts' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveBasis } from 'd3-shape' +import { isRatedEpisode, ratingBoundaries, ridgeSeasons } from './selection' +import type { RatedEpisode } from './selection' + +const colors = ['#2563eb', '#0d9488', '#d97706'] + +export const ridgelineDefinition = (input: ExampleOptions) => { + const seasons = ridgeSeasons(input.revision) + const episodes = simpsons.filter( + (row): row is RatedEpisode => + isRatedEpisode(row) && seasons.includes(row.season), + ) + const bins = binX(episodes, { + value: 'imdb_rating', + by: 'season', + thresholds: ratingBoundaries, + outputs: { count: { reduce: 'count' } }, + }) + const rows = normalize(bins, { + value: 'count', + by: 'season', + basis: 'max', + as: 'height', + }) + const overlap = 0.78 + const curve = d3Curve(curveBasis) + + return defineChart({ + marks: [ + ruleY(seasons, { + id: 'season-guides', + stroke: '#94a3b8', + strokeOpacity: 0.5, + }), + ridgelineY(rows, { + id: 'rating-ridges', + x: 'x', + y: 'season', + height: 'height', + key: (row) => `${row.season}:${row.x}`, + overlap, + color: 'season', + fillOpacity: 0.52, + strokeWidth: 1.5, + curve, + }), + ], + x: { + scale: scaleLinear().domain([4, 10]), + grid: true, + axis: { label: 'IMDb rating' }, + }, + y: { + scale: scalePoint().domain(seasons).padding(overlap), + reverse: true, + axis: { + ticks: { + values: seasons, + format: (season) => `Season ${season}`, + }, + }, + }, + color: { + range: colors, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Ridgeline density comparison' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(ridgelineDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.test.ts b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.test.ts index 3c3d2354..1adfce94 100644 --- a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.test.ts +++ b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.test.ts @@ -181,7 +181,7 @@ describe('ridgeline density comparison', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts', + 'benchmarks/conformance/cases/62-ridgeline-density/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts index 7138b138..6c029e65 100644 --- a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts +++ b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts @@ -1,84 +1,8 @@ -import { simpsons } from '@charts-poc/demo-data/simpsons' -import { - binX, - d3Curve, - defineChart, - normalize, - ridgelineY, - ruleY, -} from '@tanstack/charts' -import { scaleLinear, scalePoint } from 'd3-scale' -import { curveBasis } from 'd3-shape' -import { isRatedEpisode, ratingBoundaries, ridgeSeasons } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { RatedEpisode } from './selection' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#0d9488', '#d97706'] +export * from './example' -export const ridgelineDefinition = (input: ConformanceInput) => { - const seasons = ridgeSeasons(input.revision) - const episodes = simpsons.filter( - (row): row is RatedEpisode => - isRatedEpisode(row) && seasons.includes(row.season), - ) - const bins = binX(episodes, { - value: 'imdb_rating', - by: 'season', - thresholds: ratingBoundaries, - outputs: { count: { reduce: 'count' } }, - }) - const rows = normalize(bins, { - value: 'count', - by: 'season', - basis: 'max', - as: 'height', - }) - const overlap = 0.78 - const curve = d3Curve(curveBasis) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - ruleY(seasons, { - id: 'season-guides', - stroke: '#94a3b8', - strokeOpacity: 0.5, - }), - ridgelineY(rows, { - id: 'rating-ridges', - x: 'x', - y: 'season', - height: 'height', - key: (row) => `${row.season}:${row.x}`, - overlap, - color: 'season', - fillOpacity: 0.52, - strokeWidth: 1.5, - curve, - }), - ], - x: { - scale: scaleLinear().domain([4, 10]), - grid: true, - axis: { label: 'IMDb rating' }, - }, - y: { - scale: scalePoint().domain(seasons).padding(overlap), - reverse: true, - axis: { - ticks: { - values: seasons, - format: (season) => `Season ${season}`, - }, - }, - }, - color: { - range: colors, - }, - }) -} - -export const mount = tanstackMount( - ridgelineDefinition, - 'Ridgeline density comparison', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/63-violin-distributions/example.tsx b/benchmarks/conformance/cases/63-violin-distributions/example.tsx new file mode 100644 index 00000000..9f616c09 --- /dev/null +++ b/benchmarks/conformance/cases/63-violin-distributions/example.tsx @@ -0,0 +1,124 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { + binY, + d3AreaXCurve, + defineChart, + dot, + groupBy, + median, + normalize, + tickY, + violinY, +} from '@tanstack/charts' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveBasis } from 'd3-shape' +import { isPenguinMass, massBoundaries, violinSpecies } from './selection' + +const colors = ['#64748b', '#0d9488', '#7c3aed'] + +export const violinDefinition = (input: ExampleOptions) => { + const observations = penguins + .filter(isPenguinMass) + .slice(input.revision * 8, input.revision * 8 + 320) + const bins = binY(observations, { + value: 'body_mass_g', + by: 'species', + thresholds: massBoundaries, + outputs: { count: { reduce: 'count' } }, + }) + const profiles = normalize(bins, { + value: 'count', + by: 'species', + basis: 'max', + as: 'width', + }) + const summaries = groupBy(observations, { + by: 'species', + outputs: { + median: { value: 'body_mass_g', reduce: median }, + }, + }) + + return defineChart({ + marks: [ + violinY(profiles, { + id: 'mass-violins', + x: 'species', + y: 'y', + width: 'width', + key: (row) => `${row.species}:${row.y}`, + span: 0.76, + color: 'species', + fillOpacity: 0.58, + curve: d3AreaXCurve(curveBasis), + }), + tickY(summaries, { + id: 'median-ticks', + x: 'species', + y: 'median', + key: 'species', + span: 0.36, + stroke: '#0f172a', + strokeWidth: 2, + }), + dot(summaries, { + id: 'median-dots', + x: 'species', + y: 'median', + key: 'species', + color: 'species', + stroke: '#ffffff', + strokeWidth: 1, + r: 3.5, + }), + ], + x: { + scale: scalePoint().domain(violinSpecies).padding(0.5), + }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, + color: { + domain: violinSpecies, + range: colors, + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Violin distribution comparison' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(violinDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'median' in datum + ? `${datum.species} · median body mass ${datum.median.toLocaleString( + 'en-US', + )} g` + : `${datum.species} · distribution at ${datum.y.toLocaleString( + 'en-US', + )} g`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/63-violin-distributions/tanstack.test.ts b/benchmarks/conformance/cases/63-violin-distributions/tanstack.test.ts index 08d6faca..86d98488 100644 --- a/benchmarks/conformance/cases/63-violin-distributions/tanstack.test.ts +++ b/benchmarks/conformance/cases/63-violin-distributions/tanstack.test.ts @@ -200,7 +200,7 @@ describe('violin distribution comparison', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/63-violin-distributions/tanstack.ts', + 'benchmarks/conformance/cases/63-violin-distributions/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts b/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts index 13fa03e6..6c029e65 100644 --- a/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts +++ b/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts @@ -1,101 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { - binY, - d3AreaXCurve, - defineChart, - dot, - groupBy, - median, - normalize, - tickY, - violinY, -} from '@tanstack/charts' -import { scaleLinear, scalePoint } from 'd3-scale' -import { curveBasis } from 'd3-shape' -import { isPenguinMass, massBoundaries, violinSpecies } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#64748b', '#0d9488', '#7c3aed'] +export * from './example' -export const violinDefinition = (input: ConformanceInput) => { - const observations = penguins - .filter(isPenguinMass) - .slice(input.revision * 8, input.revision * 8 + 320) - const bins = binY(observations, { - value: 'body_mass_g', - by: 'species', - thresholds: massBoundaries, - outputs: { count: { reduce: 'count' } }, - }) - const profiles = normalize(bins, { - value: 'count', - by: 'species', - basis: 'max', - as: 'width', - }) - const summaries = groupBy(observations, { - by: 'species', - outputs: { - median: { value: 'body_mass_g', reduce: median }, - }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - violinY(profiles, { - id: 'mass-violins', - x: 'species', - y: 'y', - width: 'width', - key: (row) => `${row.species}:${row.y}`, - span: 0.76, - color: 'species', - fillOpacity: 0.58, - curve: d3AreaXCurve(curveBasis), - }), - tickY(summaries, { - id: 'median-ticks', - x: 'species', - y: 'median', - key: 'species', - span: 0.36, - stroke: '#0f172a', - strokeWidth: 2, - }), - dot(summaries, { - id: 'median-dots', - x: 'species', - y: 'median', - key: 'species', - color: 'species', - stroke: '#ffffff', - strokeWidth: 1, - r: 3.5, - }), - ], - x: { - scale: scalePoint().domain(violinSpecies).padding(0.5), - }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, - color: { - domain: violinSpecies, - range: colors, - }, - }) -} - -export const mount = tanstackMount( - violinDefinition, - 'Violin distribution comparison', - { - format: ({ datum }) => - 'median' in datum - ? `${datum.species} · median body mass ${datum.median.toLocaleString( - 'en-US', - )} g` - : `${datum.species} · distribution at ${datum.y.toLocaleString( - 'en-US', - )} g`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx b/benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx new file mode 100644 index 00000000..01be5b28 --- /dev/null +++ b/benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx @@ -0,0 +1,108 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { + colorLegend, + defineChart, + groupBy, + mosaicY, + rect, + select, + text, +} from '@tanstack/charts' +import { format } from 'd3-format' +import { scaleLinear } from 'd3-scale' +import { survey } from '@charts-poc/demo-data/survey' +import { isMosaicResponse, mosaicResponses } from './selection' + +const percent = format('.0%') +const colors = ['#991b1b', '#ef4444', '#cbd5e1', '#60a5fa', '#1d4ed8'] + +export const marimekkoDefinition = (input?: ExampleOptions) => { + const observations = survey.filter(isMosaicResponse) + const counts = groupBy(observations, { + by: { + Question: 'Question', + Response: 'Response', + }, + outputs: { count: { reduce: 'count' } }, + }) + const cells = mosaicY(counts, { + x: 'Question', + y: 'Response', + value: 'count', + yOrder: mosaicResponses, + }) + const labels = select(cells, { + by: 'xValue', + select: 'first', + }) + + return defineChart({ + marks: [ + rect(cells, { + id: 'response-cells', + x: 'x', + x1: 'x1', + x2: 'x2', + y: 'y', + y1: 'y1', + y2: 'y2', + color: 'yValue', + key: (datum) => `${datum.xValue}:${datum.yValue}`, + inset: 1, + }), + ...(input?.preview === true + ? [] + : [ + text(labels, { + id: 'question-labels', + x: 'x', + y: () => 1.055, + text: 'Question', + key: 'xValue', + fill: '#334155', + fontSize: 11, + }), + ]), + ], + x: { + scale: scaleLinear().domain([0, 1]), + axis: { ticks: { format: percent }, label: 'Share of responses' }, + }, + y: { + scale: scaleLinear().domain([0, 1.12]), + axis: { ticks: { format: percent }, label: 'Within-question share' }, + }, + color: { + domain: mosaicResponses, + range: colors, + legend: colorLegend({ label: 'Response' }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Marimekko survey composition' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(marimekkoDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.test.ts b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.test.ts index 4340162d..7104fa50 100644 --- a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.test.ts +++ b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.test.ts @@ -176,7 +176,7 @@ describe('Marimekko survey composition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts', + 'benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts index 73410f13..6c029e65 100644 --- a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts +++ b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts @@ -1,89 +1,8 @@ -import { - colorLegend, - defineChart, - groupBy, - mosaicY, - rect, - select, - text, -} from '@tanstack/charts' -import { format } from 'd3-format' -import { scaleLinear } from 'd3-scale' -import { survey } from '@charts-poc/demo-data/survey' -import { isMosaicResponse, mosaicResponses } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const percent = format('.0%') -const colors = ['#991b1b', '#ef4444', '#cbd5e1', '#60a5fa', '#1d4ed8'] +export * from './example' -export const marimekkoDefinition = (input?: ConformanceInput) => { - const observations = survey.filter(isMosaicResponse) - const counts = groupBy(observations, { - by: { - Question: 'Question', - Response: 'Response', - }, - outputs: { count: { reduce: 'count' } }, - }) - const cells = mosaicY(counts, { - x: 'Question', - y: 'Response', - value: 'count', - yOrder: mosaicResponses, - }) - const labels = select(cells, { - by: 'xValue', - select: 'first', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - rect(cells, { - id: 'response-cells', - x: 'x', - x1: 'x1', - x2: 'x2', - y: 'y', - y1: 'y1', - y2: 'y2', - color: 'yValue', - key: (datum) => `${datum.xValue}:${datum.yValue}`, - inset: 1, - }), - ...(input?.preview === true - ? [] - : [ - text(labels, { - id: 'question-labels', - x: 'x', - y: () => 1.055, - text: 'Question', - key: 'xValue', - fill: '#334155', - fontSize: 11, - }), - ]), - ], - x: { - scale: scaleLinear().domain([0, 1]), - axis: { ticks: { format: percent }, label: 'Share of responses' }, - }, - y: { - scale: scaleLinear().domain([0, 1.12]), - axis: { ticks: { format: percent }, label: 'Within-question share' }, - }, - color: { - domain: mosaicResponses, - range: colors, - legend: colorLegend({ label: 'Response' }), - }, - }) -} - -export const catalogCase = tanstackCase( - marimekkoDefinition, - 'Marimekko survey composition', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/example.tsx b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/example.tsx new file mode 100644 index 00000000..eaa38824 --- /dev/null +++ b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/example.tsx @@ -0,0 +1,94 @@ +import { cars } from '@charts-poc/demo-data/cars' +import type { CarsRow } from '@charts-poc/demo-data/cars' +import { defineChart, dot } from '@tanstack/charts' +import { Chart } from '@tanstack/charts/react/tooltip' +import { voronoi } from '@tanstack/charts/spatial/voronoi' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleLinear } from 'd3-scale' +import type { ChartTooltipOptions } from '@tanstack/charts' + +export type CompleteCar = CarsRow & { readonly 'economy (mpg)': number } +export interface ExampleOptions { + revision?: number +} + +const colors = ['#2563eb', '#0d9488', '#d97706'] +const interactiveTooltip: ChartTooltipOptions = { + anchor: 'pointer', + items: [ + { + id: 'car', + label: 'Car', + text: (point) => `${point.datum.name} · ${cylinderLabel(point.datum)}`, + }, + ], +} + +export function createExampleChart({ revision = 0 }: ExampleOptions = {}) { + const rows = selectedCars(revision) + return defineChart( + { + marks: [ + voronoi(rows, { + id: 'nearest-cells', + x: 'weight (lb)', + y: 'economy (mpg)', + key: carKey, + color: cylinderLabel, + fillOpacity: 0.14, + stroke: '#ffffff', + strokeWidth: 1, + }), + dot(rows, { + id: 'voronoi-points', + x: 'weight (lb)', + y: 'economy (mpg)', + key: carKey, + color: cylinderLabel, + stroke: '#ffffff', + strokeWidth: 1, + r: 4, + states: [ + { + when: { focus: 'primary' }, + style: { r: 7, stroke: 'Canvas', strokeWidth: 2 }, + }, + { when: { focus: 'unmatched' }, style: { opacity: 0.45 } }, + ], + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + color: { range: colors }, + }, + { + keyboard: true, + tooltip: { use: tooltip, ...interactiveTooltip }, + }, + ) +} + +export const exampleAriaLabel = 'Voronoi nearest-point interaction' +export const chart = createExampleChart() + +export default function Example() { + return +} + +export function selectedCars(revision: number): CompleteCar[] { + return cars + .filter((row): row is CompleteCar => row['economy (mpg)'] !== null) + .slice(revision * 3, revision * 3 + 18) +} + +export function cylinderLabel(row: CarsRow): string { + return `${row.cylinders} cylinders` +} + +export function carKey(row: CarsRow): string { + return `${row.name}:${row.year}:${row['weight (lb)']}` +} diff --git a/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts index 344da8fb..57d4a25c 100644 --- a/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts @@ -1,108 +1,37 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dot, mountChart } from '@tanstack/charts' -import { voronoi } from '@tanstack/charts/spatial/voronoi' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleLinear } from 'd3-scale' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ChartHostOptions, ChartTooltipOptions } from '@tanstack/charts' +import { mountChart } from '@tanstack/charts' +import type { ChartHostOptions } from '@tanstack/charts' import type { ConformanceHandle, - ConformanceInput, ConformanceMount, ConformanceTestDriver, } from '../../types' -import { tanstackCase } from '../../shared/mount' +import { + carKey, + createExampleChart, + exampleAriaLabel, + selectedCars, +} from './example' +import type { CompleteCar } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CompleteCar = CarsRow & { - readonly 'economy (mpg)': number -} - -const colors = ['#2563eb', '#0d9488', '#d97706'] -const catalogPreviewCarKey = 'AMC Gremlin:71:2634' - -const definition = (rows: readonly CompleteCar[]) => - defineChart({ - marks: [ - voronoi(rows, { - id: 'nearest-cells', - x: 'weight (lb)', - y: 'economy (mpg)', - key: carKey, - color: cylinderLabel, - fillOpacity: 0.14, - stroke: '#ffffff', - strokeWidth: 1, - }), - dot(rows, { - id: 'voronoi-points', - x: 'weight (lb)', - y: 'economy (mpg)', - key: carKey, - color: cylinderLabel, - stroke: '#ffffff', - strokeWidth: 1, - r: 4, - states: [ - { - when: { focus: 'primary' }, - style: { r: 7, stroke: 'Canvas', strokeWidth: 2 }, - }, - { - when: { focus: 'unmatched' }, - style: { opacity: 0.45 }, - }, - ], - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - color: { - range: colors, - }, - }) - -const interactiveTooltip: ChartTooltipOptions = { - anchor: 'pointer', - items: [ - { - id: 'car', - label: 'Car', - text: (point) => `${point.datum.name} · ${cylinderLabel(point.datum)}`, - }, - ], -} +export * from './example' -export const catalogCase = tanstackCase( - (input) => definition(selectedCars(input.revision)), - 'Voronoi nearest-point interaction', - interactiveTooltip, +export const catalogCase = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { focus(scene) { return ( scene.points.find( (point) => point.markId === 'voronoi-points' && - carKey(point.datum) === catalogPreviewCarKey, + carKey(point.datum) === 'AMC Gremlin:71:2634', ) ?? null ) }, }, ) -const configuredDefinition = (rows: readonly CompleteCar[]) => - defineChart(definition(rows), { - svgAnimation: false, - keyboard: true, - tooltip: { - use: tooltip, - ...interactiveTooltip, - }, - }) - export const mount: ConformanceMount = ( container, input, @@ -110,10 +39,10 @@ export const mount: ConformanceMount = ( let rows = selectedCars(input.revision) let focusedIds: string[] = [] const options: ChartHostOptions = { - definition: configuredDefinition(rows), + definition: createExampleChart(input), width: input.width, height: input.height, - ariaLabel: 'Voronoi nearest-point interaction', + ariaLabel: exampleAriaLabel, onFocusGroupChange(points) { focusedIds = points.map((point) => carKey(point.datum)) }, @@ -157,7 +86,7 @@ export const mount: ConformanceMount = ( rows = selectedCars(nextInput.revision) host.update({ ...options, - definition: configuredDefinition(rows), + definition: createExampleChart(nextInput), width: nextInput.width, height: nextInput.height, }) @@ -167,17 +96,3 @@ export const mount: ConformanceMount = ( }, } } - -function selectedCars(revision: number): CompleteCar[] { - return cars - .filter((row): row is CompleteCar => row['economy (mpg)'] !== null) - .slice(revision * 3, revision * 3 + 18) -} - -function cylinderLabel(row: CarsRow): string { - return `${row.cylinders} cylinders` -} - -function carKey(row: CarsRow): string { - return `${row.name}:${row.year}:${row['weight (lb)']}` -} diff --git a/benchmarks/conformance/cases/70-composed-chart/example.tsx b/benchmarks/conformance/cases/70-composed-chart/example.tsx new file mode 100644 index 00000000..42af244b --- /dev/null +++ b/benchmarks/conformance/cases/70-composed-chart/example.tsx @@ -0,0 +1,85 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { weather } from '@charts-poc/demo-data/weather' +import { areaY, barY, d3Curve, defineChart, dot, lineY } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import { curveMonotoneX } from 'd3-shape' + +const monotone = d3Curve(curveMonotoneX) +const dateFormat = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', +}) + +export const composedChartDefinition = (input: ExampleOptions) => { + const start = input.revision % 2 === 0 ? 37 : 68 + const rows = weather.slice(start, start + 6) + + return defineChart({ + marks: [ + areaY(rows, { + x: 'date', + y: 'temp_max', + fill: '#8884d8', + fillOpacity: 0.2, + stroke: '#8884d8', + curve: monotone, + }), + barY(rows, { + id: 'precipitation-bars', + x: 'date', + y: 'precipitation', + fill: '#413ea0', + maxThickness: 20, + }), + lineY(rows, { + x: 'date', + y: 'temp_min', + stroke: '#ff7300', + strokeWidth: 2, + curve: monotone, + }), + dot(rows, { + x: 'date', + y: 'wind', + fill: '#ef4444', + r: 4.5, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), + axis: { + ticks: { format: (value: Date) => dateFormat.format(value) }, + }, + }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, + margin: { top: 20, right: 20, bottom: 50, left: 80 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Layered Seattle weather' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(composedChartDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts b/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts index 5aac6fd3..ec26dd1d 100644 --- a/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts +++ b/benchmarks/conformance/cases/70-composed-chart/tanstack.test.ts @@ -46,7 +46,7 @@ describe('native composed-chart bar sizing', () => { const closure = await loadTanStackSources('70-composed-chart') const source = closure.files.map((file) => file.source).join('\n') - expect(closure.files.map((file) => file.path)).toEqual(['tanstack.ts']) + expect(closure.files.map((file) => file.path)).toEqual(['example.tsx']) expect(closure.roles.support.files).toBe(0) expect(source).toContain('maxThickness: 20') expect(source).not.toContain('defineChart(({') diff --git a/benchmarks/conformance/cases/70-composed-chart/tanstack.ts b/benchmarks/conformance/cases/70-composed-chart/tanstack.ts index 8d6c7976..6c029e65 100644 --- a/benchmarks/conformance/cases/70-composed-chart/tanstack.ts +++ b/benchmarks/conformance/cases/70-composed-chart/tanstack.ts @@ -1,66 +1,8 @@ -import { weather } from '@charts-poc/demo-data/weather' -import { areaY, barY, d3Curve, defineChart, dot, lineY } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { curveMonotoneX } from 'd3-shape' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const monotone = d3Curve(curveMonotoneX) -const dateFormat = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - timeZone: 'UTC', -}) +export * from './example' -export const composedChartDefinition = (input: ConformanceInput) => { - const start = input.revision % 2 === 0 ? 37 : 68 - const rows = weather.slice(start, start + 6) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - areaY(rows, { - x: 'date', - y: 'temp_max', - fill: '#8884d8', - fillOpacity: 0.2, - stroke: '#8884d8', - curve: monotone, - }), - barY(rows, { - id: 'precipitation-bars', - x: 'date', - y: 'precipitation', - fill: '#413ea0', - maxThickness: 20, - }), - lineY(rows, { - x: 'date', - y: 'temp_min', - stroke: '#ff7300', - strokeWidth: 2, - curve: monotone, - }), - dot(rows, { - x: 'date', - y: 'wind', - fill: '#ef4444', - r: 4.5, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - axis: { - ticks: { format: (value: Date) => dateFormat.format(value) }, - }, - }, - y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, - margin: { top: 20, right: 20, bottom: 50, left: 80 }, - }) -} - -export const catalogCase = tanstackCase( - composedChartDefinition, - 'Layered Seattle weather', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx b/benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx new file mode 100644 index 00000000..351c5e19 --- /dev/null +++ b/benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx @@ -0,0 +1,80 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { barX, defineChart, groupBy, stack } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import { isSexedPenguin, pyramidSexes } from './selection' + +const colors = ['#2563eb', '#db2777'] + +export const populationPyramidDefinition = (input: ExampleOptions) => { + const sourceRows = input.revision % 2 === 0 ? penguins : penguins.slice(0, -8) + const observations = sourceRows.filter(isSexedPenguin) + const counts = groupBy(observations, { + by: { + species: 'species', + sex: 'sex', + }, + outputs: { count: { reduce: 'count' } }, + }) + + return defineChart({ + marks: [ + barX(counts, { + id: 'population-bars', + x: (row) => (row.sex === 'MALE' ? -row.count : row.count), + y: 'species', + z: 'sex', + color: 'sex', + layout: stack({ offset: 'diverging', order: pyramidSexes }), + key: (row) => `${row.species}:${row.sex}`, + inset: 0.5, + }), + ], + x: { + scale: scaleLinear().domain([-80, 80]), + grid: true, + axis: { + ticks: { + count: 5, + format: (value) => Math.abs(value).toLocaleString('en-US'), + }, + label: 'Penguins observed', + }, + }, + y: { + scale: () => scaleBand().paddingInner(0.02).paddingOuter(0.01), + }, + color: { + domain: pyramidSexes, + range: colors, + }, + margin: { top: 20, right: 20, bottom: 70, left: 80 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Palmer penguins by species and sex' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(populationPyramidDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.test.ts b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.test.ts index b81195e0..cfba4e14 100644 --- a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.test.ts +++ b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.test.ts @@ -156,7 +156,7 @@ describe('definition-owned population pyramid', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts', + 'benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts index a8203ca6..6c029e65 100644 --- a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts +++ b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts @@ -1,61 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { barX, defineChart, groupBy, stack } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { isSexedPenguin, pyramidSexes } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#db2777'] +export * from './example' -export const populationPyramidDefinition = (input: ConformanceInput) => { - const sourceRows = input.revision % 2 === 0 ? penguins : penguins.slice(0, -8) - const observations = sourceRows.filter(isSexedPenguin) - const counts = groupBy(observations, { - by: { - species: 'species', - sex: 'sex', - }, - outputs: { count: { reduce: 'count' } }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - barX(counts, { - id: 'population-bars', - x: (row) => (row.sex === 'MALE' ? -row.count : row.count), - y: 'species', - z: 'sex', - color: 'sex', - layout: stack({ offset: 'diverging', order: pyramidSexes }), - key: (row) => `${row.species}:${row.sex}`, - inset: 0.5, - }), - ], - x: { - scale: scaleLinear().domain([-80, 80]), - grid: true, - axis: { - ticks: { - count: 5, - format: (value) => Math.abs(value).toLocaleString('en-US'), - }, - label: 'Penguins observed', - }, - }, - y: { - scale: () => scaleBand().paddingInner(0.02).paddingOuter(0.01), - }, - color: { - domain: pyramidSexes, - range: colors, - }, - margin: { top: 20, right: 20, bottom: 70, left: 80 }, - }) -} - -export const catalogCase = tanstackCase( - populationPyramidDefinition, - 'Palmer penguins by species and sex', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/72-recharts-mixed-bars/example.tsx b/benchmarks/conformance/cases/72-recharts-mixed-bars/example.tsx new file mode 100644 index 00000000..f008f575 --- /dev/null +++ b/benchmarks/conformance/cases/72-recharts-mixed-bars/example.tsx @@ -0,0 +1,84 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { weather } from '@charts-poc/demo-data/weather' +import { barY, defineChart, group } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +type BarSlot = 'stack' | 'independent' + +const dateFormat = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', +}) +const groupScale = scaleBand() + .domain(['stack', 'independent']) + .paddingInner(0.08) + +export const definition = (input: ExampleOptions) => { + const start = input.revision % 2 === 0 ? 37 : 68 + const rows = weather.slice(start, start + 7) + + return defineChart({ + marks: [ + barY(rows, { + x: 'date', + y1: 0, + y2: 'precipitation', + z: () => 'stack', + fill: '#8884d8', + layout: group({ scale: groupScale }), + inset: 1, + }), + barY(rows, { + x: 'date', + y1: 'precipitation', + y2: (row) => row.precipitation + row.wind, + z: () => 'stack', + fill: '#82ca9d', + layout: group({ scale: groupScale }), + inset: 1, + }), + barY(rows, { + x: 'date', + y: 'temp_max', + z: () => 'independent', + fill: '#ffc658', + layout: group({ scale: groupScale }), + inset: 1, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), + axis: { ticks: { format: (value) => dateFormat.format(value) } }, + }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, + margin: { top: 20, right: 20, bottom: 50, left: 80 }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Stacked and adjacent Seattle weather bars' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts b/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts index 1666b257..6c029e65 100644 --- a/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts +++ b/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts @@ -1,63 +1,8 @@ -import { weather } from '@charts-poc/demo-data/weather' -import { barY, defineChart, group } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type BarSlot = 'stack' | 'independent' +export * from './example' -const dateFormat = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - timeZone: 'UTC', -}) -const groupScale = scaleBand() - .domain(['stack', 'independent']) - .paddingInner(0.08) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const definition = (input: ConformanceInput) => { - const start = input.revision % 2 === 0 ? 37 : 68 - const rows = weather.slice(start, start + 7) - - return defineChart({ - marks: [ - barY(rows, { - x: 'date', - y1: 0, - y2: 'precipitation', - z: () => 'stack', - fill: '#8884d8', - layout: group({ scale: groupScale }), - inset: 1, - }), - barY(rows, { - x: 'date', - y1: 'precipitation', - y2: (row) => row.precipitation + row.wind, - z: () => 'stack', - fill: '#82ca9d', - layout: group({ scale: groupScale }), - inset: 1, - }), - barY(rows, { - x: 'date', - y: 'temp_max', - z: () => 'independent', - fill: '#ffc658', - layout: group({ scale: groupScale }), - inset: 1, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - axis: { ticks: { format: (value) => dateFormat.format(value) } }, - }, - y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, - margin: { top: 20, right: 20, bottom: 50, left: 80 }, - }) -} - -export const mount = tanstackMount( - definition, - 'Stacked and adjacent Seattle weather bars', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/73-many-point-scatter/example.tsx b/benchmarks/conformance/cases/73-many-point-scatter/example.tsx new file mode 100644 index 00000000..b1150114 --- /dev/null +++ b/benchmarks/conformance/cases/73-many-point-scatter/example.tsx @@ -0,0 +1,62 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, dot } from '@tanstack/charts' +import { cars } from '@charts-poc/demo-data/cars' +import type { CarsRow } from '@charts-poc/demo-data/cars' +import { scaleLinear, scaleSqrt } from 'd3-scale' +import { selectManyPointData } from './selection' + +const colors = ['#2563eb', '#7c3aed', '#db2777', '#f97316', '#0f766e'] + +export const manyPointScatterDefinition = (points: readonly CarsRow[]) => + defineChart({ + marks: [ + dot(points, { + id: 'cars', + x: 'weight (lb)', + y: '0-60 mph (s)', + color: 'cylinders', + key: (row) => JSON.stringify([row.name, row.year, row['weight (lb)']]), + r: 'displacement (cc)', + rScale: { + scale: () => scaleSqrt().range([2.25, 4.5]), + }, + fillOpacity: 0.72, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, + color: { + range: colors, + }, + margin: { top: 20, right: 20, bottom: 50, left: 80 }, + }) + +export const definition = (input: ExampleOptions) => + manyPointScatterDefinition(selectManyPointData(cars, input.revision)) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Automobile specifications scatter' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.test.ts b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.test.ts index f8cec5cb..7d34be41 100644 --- a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.test.ts +++ b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.test.ts @@ -45,7 +45,7 @@ describe('many-point scatter identity', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts', + 'benchmarks/conformance/cases/73-many-point-scatter/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts index 08090dde..6c029e65 100644 --- a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts @@ -1,57 +1,8 @@ -import { defineChart, dot } from '@tanstack/charts' -import { cars } from '@charts-poc/demo-data/cars' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import { scaleLinear, scaleSqrt } from 'd3-scale' -import { selectManyPointData } from './selection' -import { tanstackCase, tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#7c3aed', '#db2777', '#f97316', '#0f766e'] +export * from './example' -export const manyPointScatterDefinition = (points: readonly CarsRow[]) => - defineChart({ - marks: [ - dot(points, { - id: 'cars', - x: 'weight (lb)', - y: '0-60 mph (s)', - color: 'cylinders', - key: (row) => JSON.stringify([row.name, row.year, row['weight (lb)']]), - r: 'displacement (cc)', - rScale: { - scale: () => scaleSqrt().range([2.25, 4.5]), - }, - fillOpacity: 0.72, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, - y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, - color: { - range: colors, - }, - margin: { top: 20, right: 20, bottom: 50, left: 80 }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -const definition = (input: ConformanceInput) => - manyPointScatterDefinition(selectManyPointData(cars, input.revision)) - -const catalogDefinition = (input: ConformanceInput) => - manyPointScatterDefinition( - samplePreviewData(selectManyPointData(cars, input.revision), input, 80, [ - (row) => row['weight (lb)'], - (row) => row['0-60 mph (s)'], - (row) => row['displacement (cc)'], - ]), - ) - -export const mount = tanstackMount( - definition, - 'Automobile specifications scatter', -) - -export const catalogCase = tanstackCase( - catalogDefinition, - mount.ariaLabel, - mount.interactiveTooltip, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/74-recharts-treemap/example.tsx b/benchmarks/conformance/cases/74-recharts-treemap/example.tsx new file mode 100644 index 00000000..65d083b4 --- /dev/null +++ b/benchmarks/conformance/cases/74-recharts-treemap/example.tsx @@ -0,0 +1,61 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { treemap } from '@tanstack/charts/hierarchy/treemap' +import { flare } from '@charts-poc/demo-data/flare' +import { selectTreemapData } from './selection' + +const colors = ['#2563eb', '#8b5cf6', '#10b981'] + +const rows = selectTreemapData(flare) + +export const treemapDefinition = (input?: ExampleOptions) => + defineChart({ + marks: [ + treemap(rows, { + id: 'treemap-cells', + path: 'name', + delimiter: '.', + value: 'size', + ratio: 4 / 3, + round: true, + color: (node) => node.ancestorIds.at(-1) ?? node.id, + inset: 1, + stroke: '#ffffff', + strokeWidth: 1, + label: input?.preview === true ? undefined : 'name', + labelFill: '#ffffff', + labelFontSize: 8, + labelFontWeight: 600, + }), + ], + color: { range: colors }, + guides: false, + margin: 0, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Flare analytics treemap' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(treemapDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/74-recharts-treemap/tanstack.test.ts b/benchmarks/conformance/cases/74-recharts-treemap/tanstack.test.ts index 1f7bf508..75315ff3 100644 --- a/benchmarks/conformance/cases/74-recharts-treemap/tanstack.test.ts +++ b/benchmarks/conformance/cases/74-recharts-treemap/tanstack.test.ts @@ -79,7 +79,7 @@ describe('native responsive treemap', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts', + 'benchmarks/conformance/cases/74-recharts-treemap/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts b/benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts index 83ff4bd2..6c029e65 100644 --- a/benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts +++ b/benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts @@ -1,37 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { treemap } from '@tanstack/charts/hierarchy/treemap' -import { flare } from '@charts-poc/demo-data/flare' -import { selectTreemapData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#8b5cf6', '#10b981'] +export * from './example' -const rows = selectTreemapData(flare) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const treemapDefinition = (input?: ConformanceInput) => - defineChart({ - marks: [ - treemap(rows, { - id: 'treemap-cells', - path: 'name', - delimiter: '.', - value: 'size', - ratio: 4 / 3, - round: true, - color: (node) => node.ancestorIds.at(-1) ?? node.id, - inset: 1, - stroke: '#ffffff', - strokeWidth: 1, - label: input?.preview === true ? undefined : 'name', - labelFill: '#ffffff', - labelFontSize: 8, - labelFontWeight: 600, - }), - ], - color: { range: colors }, - guides: false, - margin: 0, - }) - -export const mount = tanstackMount(treemapDefinition, 'Flare analytics treemap') +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/75-radar/example.tsx b/benchmarks/conformance/cases/75-radar/example.tsx new file mode 100644 index 00000000..21df3837 --- /dev/null +++ b/benchmarks/conformance/cases/75-radar/example.tsx @@ -0,0 +1,133 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, normalize, select } from '@tanstack/charts' +import { fold } from '@tanstack/charts/transform/fold' +import { + angleGrid, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { decathlon } from '@charts-poc/demo-data/decathlon' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { radarEvents, timedEvents } from './selection' +import type { PolarGuideLabelContext } from '@tanstack/charts/polar' + +const ringValues = [0.2, 0.4, 0.6, 0.8, 1] as const +const angleScale = scalePoint().domain(radarEvents) +const radiusScale = scaleLinear().domain([0, 1]) + +export const foldedDecathlon = fold(decathlon, { + fields: radarEvents, + as: { key: 'event', value: 'result' }, +}) +export const normalizedDecathlon = normalize(foldedDecathlon, { + by: 'event', + value: (datum) => + timedEvents.has(datum.event) ? -datum.result : datum.result, + basis: 'extent', + as: 'relativePerformance', +}) +export const radarProfile = select(normalizedDecathlon, { + by: 'event', + select: 'first', +}) + +function angleLabelIsTopOrBottom(angle: number): boolean { + return Math.abs(Math.sin(angle)) <= Math.SQRT1_2 +} + +function angleLabelDy({ angle, y }: PolarGuideLabelContext): number { + if (!angleLabelIsTopOrBottom(angle)) return 1.1 + return y > 0 ? -1.1 : 0 +} + +export const radarDefinition = (input: ExampleOptions) => { + const margin = + input.width < 480 + ? { top: 20, right: 55, bottom: 20, left: 105 } + : { top: 20, right: 20, bottom: 20, left: 20 } + + return defineChart({ + marks: [ + polar({ + angle: { scale: angleScale, wrap: true }, + radius: { scale: radiusScale }, + inset: 0, + radiusRatio: input.preview === true ? 0.94 : 0.8, + guides: [ + radialGrid({ + values: ringValues, + shape: 'polygon', + labels: input.preview !== true, + labelAngle: Math.PI / 3, + labelRotate: 60, + labelBaseline: 'auto', + labelFontSize: 12, + format: (value) => String(Number(value) * 100), + labelFill: '#cccccc', + stroke: '#cbd5e1', + }), + angleGrid({ + values: radarEvents, + labels: input.preview !== true, + labelOffset: 8, + labelDy: angleLabelDy, + labelFill: '#808080', + labelFontSize: 12, + format: String, + stroke: '#cbd5e1', + }), + ], + marks: [ + radialArea(radarProfile, { + id: 'athlete-profile', + angle: 'event', + radius: 'relativePerformance', + key: 'event', + className: 'ts-chart__radar', + curve: curveLinearClosed, + fill: '#8884d8', + fillOpacity: 0.6, + stroke: '#8884d8', + strokeWidth: 2, + }), + ], + }), + ], + margin: input.preview === true ? 0 : margin, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Decathlon radar chart' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(radarDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.Country} · ${datum.event} · ${(datum.relativePerformance * 100).toFixed(1)}%`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/75-radar/tanstack.test.ts b/benchmarks/conformance/cases/75-radar/tanstack.test.ts index 81668128..4a0b8529 100644 --- a/benchmarks/conformance/cases/75-radar/tanstack.test.ts +++ b/benchmarks/conformance/cases/75-radar/tanstack.test.ts @@ -104,7 +104,7 @@ describe('folded radar profile', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/75-radar/tanstack.ts', + 'benchmarks/conformance/cases/75-radar/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/75-radar/tanstack.ts b/benchmarks/conformance/cases/75-radar/tanstack.ts index e102ff34..6c029e65 100644 --- a/benchmarks/conformance/cases/75-radar/tanstack.ts +++ b/benchmarks/conformance/cases/75-radar/tanstack.ts @@ -1,112 +1,8 @@ -import { defineChart, normalize, select } from '@tanstack/charts' -import { fold } from '@tanstack/charts/transform/fold' -import { - angleGrid, - polar, - radialArea, - radialGrid, -} from '@tanstack/charts/polar' -import { decathlon } from '@charts-poc/demo-data/decathlon' -import { scaleLinear, scalePoint } from 'd3-scale' -import { curveLinearClosed } from 'd3-shape' -import { radarEvents, timedEvents } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { PolarGuideLabelContext } from '@tanstack/charts/polar' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const ringValues = [0.2, 0.4, 0.6, 0.8, 1] as const -const angleScale = scalePoint().domain(radarEvents) -const radiusScale = scaleLinear().domain([0, 1]) +export * from './example' -export const foldedDecathlon = fold(decathlon, { - fields: radarEvents, - as: { key: 'event', value: 'result' }, -}) -export const normalizedDecathlon = normalize(foldedDecathlon, { - by: 'event', - value: (datum) => - timedEvents.has(datum.event) ? -datum.result : datum.result, - basis: 'extent', - as: 'relativePerformance', -}) -export const radarProfile = select(normalizedDecathlon, { - by: 'event', - select: 'first', -}) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -function angleLabelIsTopOrBottom(angle: number): boolean { - return Math.abs(Math.sin(angle)) <= Math.SQRT1_2 -} - -function angleLabelDy({ angle, y }: PolarGuideLabelContext): number { - if (!angleLabelIsTopOrBottom(angle)) return 1.1 - return y > 0 ? -1.1 : 0 -} - -export const radarDefinition = (input: ConformanceInput) => { - const margin = - input.width < 480 - ? { top: 20, right: 55, bottom: 20, left: 105 } - : { top: 20, right: 20, bottom: 20, left: 20 } - - return defineChart({ - marks: [ - polar({ - angle: { scale: angleScale, wrap: true }, - radius: { scale: radiusScale }, - inset: 0, - radiusRatio: input.preview === true ? 0.94 : 0.8, - guides: [ - radialGrid({ - values: ringValues, - shape: 'polygon', - labels: input.preview !== true, - labelAngle: Math.PI / 3, - labelRotate: 60, - labelBaseline: 'auto', - labelFontSize: 12, - format: (value) => String(Number(value) * 100), - labelFill: '#cccccc', - stroke: '#cbd5e1', - }), - angleGrid({ - values: radarEvents, - labels: input.preview !== true, - labelOffset: 8, - labelDy: angleLabelDy, - labelFill: '#808080', - labelFontSize: 12, - format: String, - stroke: '#cbd5e1', - }), - ], - marks: [ - radialArea(radarProfile, { - id: 'athlete-profile', - angle: 'event', - radius: 'relativePerformance', - key: 'event', - className: 'ts-chart__radar', - curve: curveLinearClosed, - fill: '#8884d8', - fillOpacity: 0.6, - stroke: '#8884d8', - strokeWidth: 2, - }), - ], - }), - ], - margin: input.preview === true ? 0 : margin, - }) -} - -export const catalogCase = tanstackCase( - radarDefinition, - 'Decathlon radar chart', - { - format: ({ datum }) => - `${datum.Country} · ${datum.event} · ${(datum.relativePerformance * 100).toFixed(1)}%`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/76-pie/example.tsx b/benchmarks/conformance/cases/76-pie/example.tsx new file mode 100644 index 00000000..23f91cb2 --- /dev/null +++ b/benchmarks/conformance/cases/76-pie/example.tsx @@ -0,0 +1,68 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { selectPieData } from './selection' + +const colors = ['#2563eb', '#7c3aed', '#db2777', '#f59e0b'] +const percentage = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 2, +}) + +export const pieDefinition = (input: ExampleOptions) => { + const arcs = pie(selectPieData(alphabet, input.revision), { + value: 'frequency', + }) + + return defineChart({ + marks: [ + polar({ + inset: 0, + radiusRatio: 0.8, + marks: [ + radialArc(arcs, { + id: 'letter-slices', + key: 'letter', + color: 'letter', + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'English letter frequency pie' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(pieDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.letter} · ${percentage.format(datum.frequency)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/76-pie/tanstack.test.ts b/benchmarks/conformance/cases/76-pie/tanstack.test.ts index 36af14f6..8925bd9c 100644 --- a/benchmarks/conformance/cases/76-pie/tanstack.test.ts +++ b/benchmarks/conformance/cases/76-pie/tanstack.test.ts @@ -60,7 +60,7 @@ describe('native pie allocation', () => { it('keeps allocation in the public polar definition surface', () => { const source = readFileSync( - resolve(process.cwd(), 'benchmarks/conformance/cases/76-pie/tanstack.ts'), + resolve(process.cwd(), 'benchmarks/conformance/cases/76-pie/example.tsx'), 'utf8', ) diff --git a/benchmarks/conformance/cases/76-pie/tanstack.ts b/benchmarks/conformance/cases/76-pie/tanstack.ts index 29aa0ff4..6c029e65 100644 --- a/benchmarks/conformance/cases/76-pie/tanstack.ts +++ b/benchmarks/conformance/cases/76-pie/tanstack.ts @@ -1,45 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { selectPieData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#2563eb', '#7c3aed', '#db2777', '#f59e0b'] -const percentage = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 2, -}) +export * from './example' -export const pieDefinition = (input: ConformanceInput) => { - const arcs = pie(selectPieData(alphabet, input.revision), { - value: 'frequency', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - inset: 0, - radiusRatio: 0.8, - marks: [ - radialArc(arcs, { - id: 'letter-slices', - key: 'letter', - color: 'letter', - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - pieDefinition, - 'English letter frequency pie', - { - format: ({ datum }) => - `${datum.letter} · ${percentage.format(datum.frequency)}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/77-donut/example.tsx b/benchmarks/conformance/cases/77-donut/example.tsx new file mode 100644 index 00000000..efbc6965 --- /dev/null +++ b/benchmarks/conformance/cases/77-donut/example.tsx @@ -0,0 +1,69 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { selectDonutData } from './selection' + +const colors = ['#0ea5e9', '#6366f1', '#a855f7', '#ec4899', '#f97316'] +const percentage = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 2, +}) + +export const donutDefinition = (input: ExampleOptions) => { + const arcs = pie(selectDonutData(alphabet, input.revision), { + value: 'frequency', + }) + + return defineChart({ + marks: [ + polar({ + inset: 0, + radiusRatio: 0.8, + marks: [ + radialArc(arcs, { + id: 'letter-slices', + key: 'letter', + innerRadius: ({ radius }) => radius * 0.58, + color: 'letter', + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'English letter frequency donut' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(donutDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.letter} · ${percentage.format(datum.frequency)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/77-donut/tanstack.test.ts b/benchmarks/conformance/cases/77-donut/tanstack.test.ts index 134a931a..2eb61d1c 100644 --- a/benchmarks/conformance/cases/77-donut/tanstack.test.ts +++ b/benchmarks/conformance/cases/77-donut/tanstack.test.ts @@ -66,7 +66,7 @@ describe('native donut allocation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/77-donut/tanstack.ts', + 'benchmarks/conformance/cases/77-donut/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/77-donut/tanstack.ts b/benchmarks/conformance/cases/77-donut/tanstack.ts index 3cefcbf7..6c029e65 100644 --- a/benchmarks/conformance/cases/77-donut/tanstack.ts +++ b/benchmarks/conformance/cases/77-donut/tanstack.ts @@ -1,46 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { selectDonutData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = ['#0ea5e9', '#6366f1', '#a855f7', '#ec4899', '#f97316'] -const percentage = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 2, -}) +export * from './example' -export const donutDefinition = (input: ConformanceInput) => { - const arcs = pie(selectDonutData(alphabet, input.revision), { - value: 'frequency', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - inset: 0, - radiusRatio: 0.8, - marks: [ - radialArc(arcs, { - id: 'letter-slices', - key: 'letter', - innerRadius: ({ radius }) => radius * 0.58, - color: 'letter', - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - donutDefinition, - 'English letter frequency donut', - { - format: ({ datum }) => - `${datum.letter} · ${percentage.format(datum.frequency)}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/78-gauge/example.tsx b/benchmarks/conformance/cases/78-gauge/example.tsx new file mode 100644 index 00000000..f87354a7 --- /dev/null +++ b/benchmarks/conformance/cases/78-gauge/example.tsx @@ -0,0 +1,73 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { survey } from '@charts-poc/demo-data/survey' +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc } from '@tanstack/charts/polar' +import { agreementPercent, gaugeSegments } from './transform' +import type { GaugeDatum } from './transform' + +const startAngle = (-Math.PI * 3) / 4 +const endAngle = (Math.PI * 3) / 4 +const ids: readonly GaugeDatum['id'][] = ['value', 'remainder'] +const colors = ['#ef4444', '#e2e8f0'] + +export const gaugeDefinition = (input: ExampleOptions) => { + const question = `Q${(input.revision % 2) + 1}` + const agreement = agreementPercent(survey, question) + const segments = gaugeSegments(agreement) + const arcs = pie(segments, { + value: 'value', + startAngle, + endAngle, + }) + + return defineChart({ + marks: [ + polar({ + inset: 0, + radiusRatio: 0.8, + marks: [ + radialArc(arcs, { + id: 'gauge-segments', + key: 'id', + innerRadius: ({ radius }) => radius * 0.72, + color: 'id', + }), + ], + }), + ], + color: { domain: ids, range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Survey agreement share gauge' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(gaugeDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => `${datum.label} · ${datum.value}%`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/78-gauge/tanstack.test.ts b/benchmarks/conformance/cases/78-gauge/tanstack.test.ts index 1563a60a..5e0f8246 100644 --- a/benchmarks/conformance/cases/78-gauge/tanstack.test.ts +++ b/benchmarks/conformance/cases/78-gauge/tanstack.test.ts @@ -83,7 +83,7 @@ describe('native gauge allocation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/78-gauge/tanstack.ts', + 'benchmarks/conformance/cases/78-gauge/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/78-gauge/tanstack.ts b/benchmarks/conformance/cases/78-gauge/tanstack.ts index ab494fc8..6c029e65 100644 --- a/benchmarks/conformance/cases/78-gauge/tanstack.ts +++ b/benchmarks/conformance/cases/78-gauge/tanstack.ts @@ -1,52 +1,8 @@ -import { survey } from '@charts-poc/demo-data/survey' -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc } from '@tanstack/charts/polar' -import { agreementPercent, gaugeSegments } from './transform' -import { tanstackCase } from '../../shared/mount' -import type { GaugeDatum } from './transform' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const startAngle = (-Math.PI * 3) / 4 -const endAngle = (Math.PI * 3) / 4 -const ids: readonly GaugeDatum['id'][] = ['value', 'remainder'] -const colors = ['#ef4444', '#e2e8f0'] +export * from './example' -export const gaugeDefinition = (input: ConformanceInput) => { - const question = `Q${(input.revision % 2) + 1}` - const agreement = agreementPercent(survey, question) - const segments = gaugeSegments(agreement) - const arcs = pie(segments, { - value: 'value', - startAngle, - endAngle, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - inset: 0, - radiusRatio: 0.8, - marks: [ - radialArc(arcs, { - id: 'gauge-segments', - key: 'id', - innerRadius: ({ radius }) => radius * 0.72, - color: 'id', - }), - ], - }), - ], - color: { domain: ids, range: colors }, - margin: 0, - }) -} - -export const catalogCase = tanstackCase( - gaugeDefinition, - 'Survey agreement share gauge', - { - format: ({ datum }) => `${datum.label} · ${datum.value}%`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/80-echarts-axis-pointer/example.tsx b/benchmarks/conformance/cases/80-echarts-axis-pointer/example.tsx new file mode 100644 index 00000000..c5af0005 --- /dev/null +++ b/benchmarks/conformance/cases/80-echarts-axis-pointer/example.tsx @@ -0,0 +1,154 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { industries } from '@charts-poc/demo-data/industries' +import { colorLegend, defineChart, dot, lineY } from '@tanstack/charts' +import { focusGuideX } from '@tanstack/charts/focus/guide' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { axisPointerColors } from './colors' +import { axisPointerDateKey } from './model' +import { axisPointerData, axisPointerIndustries } from './selection' +import type { ChartTooltipOptions } from '@tanstack/charts' +import type { AxisPointerDatum } from './selection' +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const createExampleChart = axisPointerDefinition +export const exampleAriaLabel = 'Snapped axis pointer with grouped tooltip' + +const month = new Intl.DateTimeFormat('en-US', { + month: 'short', + timeZone: 'UTC', +}) + +const monthYear = new Intl.DateTimeFormat('en-US', { + month: 'short', + year: 'numeric', + timeZone: 'UTC', +}) + +const axisPointerTooltip: ChartTooltipOptions = { + className: 'conformance-tooltip-grouped', + sticky: false, + anchor: { x: 'value', y: 'plot-top' }, + placement: ['bottom-right', 'bottom-left', 'right', 'left'], + offset: 10, + sort: 'color-domain', + items: [ + { + channel: 'x', + label: '', + text: (point) => monthYear.format(point.datum.date), + }, + { + channel: 'y', + text: (point) => point.datum.unemployed.toLocaleString('en-US'), + }, + { channel: 'group', label: 'Industry' }, + ], +} + +export function axisPointerDefinition(input: ExampleChartInput) { + const rows = axisPointerData(industries, input.revision) + + return defineChart({ + marks: [ + lineY(rows, { + id: 'industry-lines', + x: 'date', + y: 'unemployed', + z: 'industry', + color: 'industry', + key: axisPointerKey, + strokeWidth: 2, + }), + dot(rows, { + id: 'industry-points', + x: 'date', + y: 'unemployed', + z: 'industry', + color: 'industry', + key: axisPointerKey, + r: 3, + stroke: '#ffffff', + strokeWidth: 1, + }), + focusGuideX(rows, { + id: 'axis-pointer-guide', + x: 'date', + y: 'unemployed', + z: 'industry', + key: axisPointerKey, + xRule: { + stroke: '#64748b', + strokeWidth: 1, + strokeDasharray: '4 4', + }, + }), + ], + x: { + scale: scaleUtc, + axis: { ticks: { format: (value) => month.format(value) } }, + }, + y: { + scale: scaleLinear, + grid: input.preview !== true, + axis: { + ticks: { count: 5 }, + ...(input.preview === true ? {} : { label: 'Unemployed (thousands)' }), + }, + }, + color: { + domain: axisPointerIndustries, + range: axisPointerIndustries.map( + (industry) => axisPointerColors[industry], + ), + legend: colorLegend({ itemWidth: 100 }), + }, + focus: 'group-x', + focusRing: false, + maxFocusDistance: Number.POSITIVE_INFINITY, + svgAnimation: false, + keyboard: true, + tooltip: { + use: tooltip, + ...axisPointerTooltip, + }, + margin: + input.preview === true + ? { top: 4, right: 4, bottom: 22, left: 38 } + : { top: 38, right: 24, bottom: 45, left: 60 }, + }) +} + +function axisPointerKey(row: AxisPointerDatum) { + return `${row.industry}:${axisPointerDateKey(row.date)}` +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, +}) + +export default function Example() { + return ( + + ) +} diff --git a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.test.ts b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.test.ts index 0394018e..bc33e65e 100644 --- a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.test.ts +++ b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.test.ts @@ -202,12 +202,11 @@ describe('definition-owned snapped axis pointer', () => { process.cwd(), 'benchmarks/conformance/cases/80-echarts-axis-pointer', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') expect(existsSync(resolve(directory, 'view.tsx'))).toBe(false) for (const forbidden of [ "from 'react'", - '@tanstack/charts/react', 'useState', 'onRender', 'positionTooltip', diff --git a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts index 14471c2e..b663d013 100644 --- a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts +++ b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts @@ -1,14 +1,8 @@ +import { axisPointerDefinition } from './example' +export { axisPointerDefinition } from './example' import { industries } from '@charts-poc/demo-data/industries' -import { - colorLegend, - defineChart, - dot, - lineY, - mountChart, -} from '@tanstack/charts' -import { focusGuideX } from '@tanstack/charts/focus/guide' +import { mountChart } from '@tanstack/charts' import { tooltip } from '@tanstack/charts/tooltip' -import { scaleLinear, scaleUtc } from 'd3-scale' import { clientPointBounds, scenePointToClient, @@ -39,6 +33,8 @@ import type { ConformanceTestDriver, } from '../../types' +export { default as Example } from './example' + const month = new Intl.DateTimeFormat('en-US', { month: 'short', timeZone: 'UTC', @@ -70,79 +66,6 @@ const axisPointerTooltip: ChartTooltipOptions = { ], } -export const axisPointerDefinition = (input: ConformanceInput) => { - const rows = axisPointerData(industries, input.revision) - - return defineChart({ - marks: [ - lineY(rows, { - id: 'industry-lines', - x: 'date', - y: 'unemployed', - z: 'industry', - color: 'industry', - key: axisPointerKey, - strokeWidth: 2, - }), - dot(rows, { - id: 'industry-points', - x: 'date', - y: 'unemployed', - z: 'industry', - color: 'industry', - key: axisPointerKey, - r: 3, - stroke: '#ffffff', - strokeWidth: 1, - }), - focusGuideX(rows, { - id: 'axis-pointer-guide', - x: 'date', - y: 'unemployed', - z: 'industry', - key: axisPointerKey, - xRule: { - stroke: '#64748b', - strokeWidth: 1, - strokeDasharray: '4 4', - }, - }), - ], - x: { - scale: scaleUtc, - axis: { ticks: { format: (value) => month.format(value) } }, - }, - y: { - scale: scaleLinear, - grid: input.preview !== true, - axis: { - ticks: { count: 5 }, - ...(input.preview === true ? {} : { label: 'Unemployed (thousands)' }), - }, - }, - color: { - domain: axisPointerIndustries, - range: axisPointerIndustries.map( - (industry) => axisPointerColors[industry], - ), - legend: colorLegend({ itemWidth: 100 }), - }, - focus: 'group-x', - focusRing: false, - maxFocusDistance: Number.POSITIVE_INFINITY, - svgAnimation: false, - keyboard: true, - tooltip: { - use: tooltip, - ...axisPointerTooltip, - }, - margin: - input.preview === true - ? { top: 4, right: 4, bottom: 22, left: 38 } - : { top: 38, right: 24, bottom: 45, left: 60 }, - }) -} - export const catalogCase = tanstackCase( axisPointerDefinition, 'Snapped axis pointer with grouped tooltip', @@ -221,10 +144,6 @@ export const mount: ConformanceMount = (container, input) => { } } -function axisPointerKey(row: AxisPointerDatum) { - return `${row.industry}:${axisPointerDateKey(row.date)}` -} - function resolveTarget( surface: HTMLElement, input: ConformanceInput, diff --git a/benchmarks/conformance/cases/81-recharts-interactive-legend/example.tsx b/benchmarks/conformance/cases/81-recharts-interactive-legend/example.tsx new file mode 100644 index 00000000..4c5df12b --- /dev/null +++ b/benchmarks/conformance/cases/81-recharts-interactive-legend/example.tsx @@ -0,0 +1,119 @@ +import { useMemo, useState } from 'react' +import { colorLegend, defineChart, lineY } from '@tanstack/charts' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { interactiveColorLegend } from '@tanstack/charts/legend' +import { Chart } from '@tanstack/charts/react' +import { industries } from '@charts-poc/demo-data/industries' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { legendRows, legendSeries } from './model' +import type { LegendSeriesId } from './model' + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export const yDomain = [0, 900] as const +export const initialVisibleSeries: readonly LegendSeriesId[] = [ + 'Manufacturing', + 'Construction', +] +export const seriesColors: Readonly> = { + Manufacturing: '#2563eb', + Construction: '#f97316', +} + +export function interactiveLegendDefinition( + revision: number, + visibleSeries: readonly LegendSeriesId[], + onVisibleSeriesChange: (visible: readonly LegendSeriesId[]) => void, +) { + const rows = legendRows(industries, revision) + return defineChart( + defineChart({ + marks: [ + lineY(rows, { + id: 'industry-lines', + x: 'date', + y: 'unemployed', + color: 'industry', + strokeWidth: 2.5, + }), + ], + x: { + scale: scaleUtc, + axis: { + ticks: { + format: (date) => + date.toLocaleDateString('en-US', { + month: 'short', + timeZone: 'UTC', + }), + }, + }, + }, + y: { + scale: scaleLinear().domain(yDomain), + grid: true, + axis: { ticks: { count: 5 }, label: 'Unemployed (thousands)' }, + }, + color: { + domain: legendSeries.map((series) => series.id), + range: legendSeries.map((series) => seriesColors[series.id]), + legend: interactiveColorLegend({ + visible: controlledSignal(visibleSeries, onVisibleSeriesChange), + placement: 'bottom', + ariaLabel: 'Series visibility', + }), + }, + margin: { top: 20, right: 24, left: 62 }, + }), + { keyboard: true }, + ) +} + +export default function Example({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const [visibleSeries, setVisibleSeries] = useState(initialVisibleSeries) + const definition = useMemo( + () => + interactiveLegendDefinition(revision, visibleSeries, setVisibleSeries), + [revision, visibleSeries], + ) + + return ( + + ) +} + +export function interactiveLegendPreviewDefinition(revision: number) { + const rows = legendRows(industries, revision) + return defineChart({ + marks: [ + lineY(rows, { + id: 'industry-lines', + x: 'date', + y: 'unemployed', + color: 'industry', + strokeWidth: 2.5, + }), + ], + x: { scale: scaleUtc }, + y: { scale: scaleLinear().domain(yDomain) }, + color: { + domain: legendSeries.map((series) => series.id), + range: legendSeries.map((series) => seriesColors[series.id]), + legend: colorLegend({ label: 'Series', placement: 'bottom' }), + }, + margin: 0, + }) +} diff --git a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.test.ts b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.test.ts index 10e89bd3..787e19be 100644 --- a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.test.ts +++ b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.test.ts @@ -124,7 +124,7 @@ describe('definition-owned interactive legend', () => { process.cwd(), 'benchmarks/conformance/cases/81-recharts-interactive-legend', ) - const source = readFileSync(resolve(directory, 'view.tsx'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') for (const forbidden of [ 'flatMap(', diff --git a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts index 89c3ca68..23a5cd1c 100644 --- a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts +++ b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts @@ -1 +1,2 @@ -export { interactiveLegendDefinition, mount } from './view' +export * from './example' +export { mount } from './view' diff --git a/benchmarks/conformance/cases/81-recharts-interactive-legend/view.tsx b/benchmarks/conformance/cases/81-recharts-interactive-legend/view.tsx index b152d88a..bfb815d9 100644 --- a/benchmarks/conformance/cases/81-recharts-interactive-legend/view.tsx +++ b/benchmarks/conformance/cases/81-recharts-interactive-legend/view.tsx @@ -5,82 +5,19 @@ import { useRef, useState, } from 'react' -import { colorLegend, defineChart, lineY } from '@tanstack/charts' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { interactiveColorLegend } from '@tanstack/charts/legend' import { Chart } from '@tanstack/charts/react' -import { industries } from '@charts-poc/demo-data/industries' -import { scaleLinear, scaleUtc } from 'd3-scale' import { catalogPreviewDefinition } from '../../shared/preview' import { reactMount } from '../../shared/react-mount' -import { isLegendSeriesId, legendRows, legendSeries } from './model' +import { isLegendSeriesId, legendSeries } from './model' +import { + initialVisibleSeries, + interactiveLegendDefinition, + interactiveLegendPreviewDefinition, + seriesColors, + yDomain, +} from './example' import type { ConformanceTarget, ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' -import type { LegendSeriesId } from './model' - -const yDomain = [0, 900] as const -const initialVisibleSeries: readonly LegendSeriesId[] = [ - 'Manufacturing', - 'Construction', -] -const seriesColors: Readonly> = { - Manufacturing: '#2563eb', - Construction: '#f97316', -} - -export function interactiveLegendDefinition( - revision: number, - visibleSeries: readonly LegendSeriesId[], - onVisibleSeriesChange: (visible: readonly LegendSeriesId[]) => void, - preview = false, -) { - const rows = legendRows(industries, revision) - return defineChart( - defineChart({ - marks: [ - lineY(rows, { - id: 'industry-lines', - x: 'date', - y: 'unemployed', - color: 'industry', - strokeWidth: 2.5, - }), - ], - x: { - scale: scaleUtc, - axis: { - ticks: { - format: (date) => - date.toLocaleDateString('en-US', { - month: 'short', - timeZone: 'UTC', - }), - }, - }, - }, - y: { - scale: scaleLinear().domain(yDomain), - grid: true, - axis: { ticks: { count: 5 }, label: 'Unemployed (thousands)' }, - }, - color: { - domain: legendSeries.map((series) => series.id), - range: legendSeries.map((series) => seriesColors[series.id]), - legend: preview - ? colorLegend({ label: 'Series', placement: 'bottom' }) - : interactiveColorLegend({ - visible: controlledSignal(visibleSeries, onVisibleSeriesChange), - placement: 'bottom', - ariaLabel: 'Series visibility', - }), - }, - margin: preview - ? { top: 0, right: 0, left: 0 } - : { top: 20, right: 24, left: 62 }, - }), - { svgAnimation: false, keyboard: false }, - ) -} const InteractiveLegendExample = forwardRef< ConformanceTestDriver, @@ -94,7 +31,6 @@ const InteractiveLegendExample = forwardRef< input.revision, visibleSeries, setVisibleSeries, - input.preview, ), [input.preview, input.revision, visibleSeries], ) @@ -134,10 +70,13 @@ const InteractiveLegendExample = forwardRef< return ( void, +) { + const rows = selectionRows(penguins, revision) + const selection = keyedSelection< + CompletePenguin, + SelectionId, + number, + number + >({ + selected: controlledSignal< + SelectionId | null, + KeyedSelectionChange + >(selectedId, (next) => onSelectedIdChange(next)), + key: (datum) => penguinSelectionId(datum), + }) + + return defineChart({ + marks: [ + dot(rows, { + id: 'observations', + x: 'flipper_length_mm', + y: 'body_mass_g', + key: selectionRowId, + r: 4.5, + fill: '#2563eb', + }), + whenSelected( + dot(rows, { + id: 'selected-observation', + x: 'flipper_length_mm', + y: 'body_mass_g', + key: selectionRowId, + r: 7, + fill: '#f97316', + stroke: '#ffffff', + strokeWidth: 2, + }), + selection, + ), + ], + x: { scale: scaleLinear, axis: { label: 'Flipper length (mm)' } }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 5 }, label: 'Body mass (g)' }, + }, + margin: { top: 16, right: 24, bottom: 42, left: 62 }, + svgAnimation: false, + keyboard: true, + maxFocusDistance: 40, + selection, + }) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function ChartTableExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '82-chart-table-selection' + const viewRef = useRef(null) + + const chartSurfaceRef = useRef(null) + + const renderedChartRef = useRef<{ + scene: ChartScene + svg: SVGSVGElement + } | null>(null) + + const [selectedId, setSelectedId] = useState( + false ? catalogPreviewSelectionId : null, + ) + + const rows = useMemo( + () => selectionRows(penguins, input.revision), + [input.revision], + ) + + const selectedDatum = rows.find( + (row) => penguinSelectionId(row) === selectedId, + ) + + const chartHeight = Math.max(96, input.height - 204) + + const tableHeight = Math.max(44, input.height - chartHeight - 52) + + const definition = useMemo( + () => + chartTableSelectionDefinition(input.revision, selectedId, setSelectedId), + [input.revision, selectedId], + ) + + const announcement = selectedDatum + ? `Selected ${penguinSelectionLabel(selectedDatum)}: ${selectedDatum.body_mass_g.toLocaleString()} g` + : 'No observation selected' + + return ( +
+
+ { + renderedChartRef.current = { scene, svg } + }} + /> +
+
+ + {announcement} + + +
+
+ + + + {['Island', 'Penguin', 'Body mass (g)'].map((label) => ( + + ))} + + + + {rows.map((datum) => { + const id = penguinSelectionId(datum) + if (!id) return null + const selected = id === selectedId + return ( + + + + + + ) + })} + +
+ {label} +
{datum.island} + + + {datum.body_mass_g.toLocaleString()} +
+
+
+ ) +} diff --git a/benchmarks/conformance/cases/82-chart-table-selection/tanstack.test.ts b/benchmarks/conformance/cases/82-chart-table-selection/tanstack.test.ts index 71573726..9efb781e 100644 --- a/benchmarks/conformance/cases/82-chart-table-selection/tanstack.test.ts +++ b/benchmarks/conformance/cases/82-chart-table-selection/tanstack.test.ts @@ -128,13 +128,20 @@ describe('definition-owned chart selection', () => { }) it('keeps the semantic table in the app and removes selection plumbing', () => { - const source = readFileSync( + const shellSource = readFileSync( resolve( process.cwd(), 'benchmarks/conformance/cases/82-chart-table-selection/view.tsx', ), 'utf8', ) + const exampleSource = readFileSync( + resolve( + process.cwd(), + 'benchmarks/conformance/cases/82-chart-table-selection/example.tsx', + ), + 'utf8', + ) for (const forbidden of [ 'rows.filter(', @@ -142,12 +149,14 @@ describe('definition-owned chart selection', () => { '...(selectedRows', 'onSelect=', ]) { - expect(source).not.toContain(forbidden) + expect(exampleSource).not.toContain(forbidden) + expect(shellSource).not.toContain(forbidden) } - expect(source).toContain('keyedSelection') - expect(source).toContain('whenSelected') - expect(source).toContain(' void, -) { - const rows = selectionRows(penguins, revision) - const selection = keyedSelection< - CompletePenguin, - SelectionId, - number, - number - >({ - selected: controlledSignal< - SelectionId | null, - KeyedSelectionChange - >(selectedId, (next) => onSelectedIdChange(next)), - key: (datum) => penguinSelectionId(datum), - }) - - return defineChart({ - marks: [ - dot(rows, { - id: 'observations', - x: 'flipper_length_mm', - y: 'body_mass_g', - key: selectionRowId, - r: 4.5, - fill: '#2563eb', - }), - whenSelected( - dot(rows, { - id: 'selected-observation', - x: 'flipper_length_mm', - y: 'body_mass_g', - key: selectionRowId, - r: 7, - fill: '#f97316', - stroke: '#ffffff', - strokeWidth: 2, - }), - selection, - ), - ], - x: { scale: scaleLinear, axis: { label: 'Flipper length (mm)' } }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 5 }, label: 'Body mass (g)' }, - }, - margin: { top: 16, right: 24, bottom: 42, left: 62 }, - svgAnimation: false, - keyboard: true, - maxFocusDistance: 40, - selection, - }) -} - const ChartTableExample = forwardRef< ConformanceTestDriver, ReactConformanceProps diff --git a/benchmarks/conformance/cases/83-focus-context-window/example.tsx b/benchmarks/conformance/cases/83-focus-context-window/example.tsx new file mode 100644 index 00000000..a89971ec --- /dev/null +++ b/benchmarks/conformance/cases/83-focus-context-window/example.tsx @@ -0,0 +1,321 @@ +import { useCallback, useMemo, useRef, useState } from 'react' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { brushX } from '@tanstack/charts/interaction/brush' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { keyedSelection, whenSelected } from '@tanstack/charts/selection' +import { Chart } from '@tanstack/charts/react' +import { aapl } from '@charts-poc/demo-data/aapl' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { + dateKey, + focusContextDomain, + initialFocusContextWindow, + monthlyAaplRows, + rowsInWindow, + windowForDate, +} from './model' +import type { ChartScene } from '@tanstack/charts' +import type { + BrushRange, + BrushXChange, +} from '@tanstack/charts/interaction/brush' +import type { KeyedSelectionChange } from '@tanstack/charts/selection' +import type { AaplRow } from '@charts-poc/demo-data/aapl' +import type { FocusContextWindow } from './model' + +interface BrushStatus { + dragging: boolean + outcome: 'idle' | 'dragging' | 'commit' | 'cancel' +} + +const detailMargin = { top: 16, right: 24, bottom: 38, left: 52 } + +const overviewMargin = { top: 8, right: 24, bottom: 22, left: 52 } + +const gap = 8 + +const focusContextRows = monthlyAaplRows(aapl) + +const focusContextDates = focusContextRows.map((row) => row.Date) + +const fullDomain = focusContextDomain(focusContextRows) + +export function focusContextDetailDefinition(window: FocusContextWindow) { + const rows = rowsInWindow(focusContextRows, window) + const selection = keyedSelection({ + selected: controlledSignal< + string | null, + KeyedSelectionChange + >(dateKey(window.selected), () => {}), + key: (row) => dateKey(row.Date), + }) + return defineChart( + defineChart({ + marks: [ + lineY(rows, { + id: 'detail-line', + x: 'Date', + y: 'Close', + stroke: '#2563eb', + strokeWidth: 2.5, + }), + dot(rows, { + id: 'detail-points', + x: 'Date', + y: 'Close', + fill: '#2563eb', + r: 3, + }), + whenSelected( + dot(rows, { + id: 'selected-point', + x: 'Date', + y: 'Close', + fill: '#f97316', + stroke: '#ffffff', + strokeWidth: 2, + r: 6, + }), + selection, + ), + ], + x: { + scale: scaleUtc().domain([window.start, window.end]), + axis: { label: 'Selected time window' }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { label: 'Close ($)' }, + }, + margin: detailMargin, + }), + { svgAnimation: false, keyboard: false }, + ) +} + +export function focusContextOverviewDefinition( + window: FocusContextWindow, + onChange: (range: BrushRange, reason: BrushXChange) => void, +) { + return defineChart( + defineChart({ + marks: [ + lineY(focusContextRows, { + id: 'overview-line', + x: 'Date', + y: 'Close', + stroke: '#2563eb', + strokeWidth: 1.75, + }), + ], + x: { + scale: scaleUtc().domain(fullDomain), + axis: { + ticks: { + count: 4, + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + timeZone: 'UTC', + }), + }, + }, + }, + y: { scale: scaleLinear, axis: false }, + margin: overviewMargin, + controls: [ + brushX({ + id: 'focus-window', + range: controlledSignal, BrushXChange>( + { start: window.start, end: window.end }, + (next, { reason }) => onChange(next, reason), + ), + values: focusContextDates, + ariaLabel: 'Selected time window', + startAriaLabel: 'Selected time window start', + endAriaLabel: 'Selected time window end', + format: monthLabel, + handleSize: 16, + selectionStyle: { + fill: '#2563eb', + fillOpacity: 0.16, + stroke: '#2563eb', + strokeWidth: 1.5, + }, + handleStyle: { fill: '#2563eb', fillOpacity: 0.9 }, + }), + ], + }), + { svgAnimation: false, keyboard: false }, + ) +} + +function viewHeights(height: number) { + const controls = 52 + const overview = Math.max(56, Math.min(100, Math.round(height * 0.24))) + return { + detail: Math.max(1, height - overview - controls - gap * 2), + overview, + controls, + } +} + +function rangeLabel(window: FocusContextWindow) { + return `${monthLabel(window.start)} – ${monthLabel(window.end)}` +} + +function monthLabel(date: Date) { + return date.toLocaleDateString(undefined, { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }) +} + +function selectedIndex(window: FocusContextWindow) { + return Math.max( + 0, + focusContextDates.findIndex( + (date) => date.getTime() === window.selected.getTime(), + ), + ) +} + +function nearestDateInRange(range: BrushRange) { + const midpoint = (range.start.getTime() + range.end.getTime()) / 2 + return focusContextDates.reduce((candidate, date) => + Math.abs(date.getTime() - midpoint) < + Math.abs(candidate.getTime() - midpoint) + ? date + : candidate, + ) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function FocusContextExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '83-focus-context-window' + const detailSurfaceRef = useRef(null) + + const overviewSurfaceRef = useRef(null) + + const overviewSceneRef = useRef | null>(null) + + const windowRef = useRef(initialFocusContextWindow(focusContextDates)) + + const brushStatusRef = useRef({ + dragging: false, + outcome: 'idle', + }) + + const [window, setWindowState] = useState(windowRef.current) + + const heights = viewHeights(input.height) + + const detailDefinition = useMemo( + () => focusContextDetailDefinition(window), + [window.start, window.end, window.selected], + ) + + const chooseDate = useCallback((date: Date) => { + const next = windowForDate(focusContextDates, date) + windowRef.current = next + setWindowState(next) + }, []) + + const handleBrushChange = useCallback( + (range: BrushRange, reason: BrushXChange) => { + brushStatusRef.current = { + dragging: reason.type === 'preview', + outcome: reason.type === 'preview' ? 'dragging' : reason.type, + } + if (reason.type !== 'commit') return + chooseDate(nearestDateInRange(range)) + }, + [chooseDate], + ) + + const overviewDefinition = useMemo( + () => focusContextOverviewDefinition(window, handleBrushChange), + [handleBrushChange, window.end, window.start], + ) + + return ( +
+
+ +
+
+ { + overviewSceneRef.current = scene + }} + /> +
+
+ { + const date = focusContextDates[Number(event.currentTarget.value)] + if (date) chooseDate(date) + }} + style={{ width: '100%', minHeight: 44, cursor: 'pointer' }} + /> + + {rangeLabel(window)} + +
+
+ ) +} diff --git a/benchmarks/conformance/cases/83-focus-context-window/tanstack.test.ts b/benchmarks/conformance/cases/83-focus-context-window/tanstack.test.ts index 1b0d70b4..d5fa753b 100644 --- a/benchmarks/conformance/cases/83-focus-context-window/tanstack.test.ts +++ b/benchmarks/conformance/cases/83-focus-context-window/tanstack.test.ts @@ -114,13 +114,12 @@ describe('definition-owned focus/context brush', () => { process.cwd(), 'benchmarks/conformance/cases/83-focus-context-window', ) - const entry = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const entry = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const source = readFileSync(resolve(directory, 'view.tsx'), 'utf8') - expect(entry).toContain("from './view'") + expect(source).toContain("from './example'") expect(entry).toContain('focusContextDetailDefinition') expect(entry).toContain('focusContextOverviewDefinition') - expect(entry).not.toContain('brushX(') for (const forbidden of [ "from 'd3-brush'", @@ -133,14 +132,15 @@ describe('definition-owned focus/context brush', () => { ' { expect(source).toContain( 'definition={catalogPreviewDefinition(overviewDefinition)}', ) - expect(source).not.toContain("from '@tanstack/charts/view'") - expect(source).not.toContain('viewGrid(') + expect(entry).not.toContain("from '@tanstack/charts/view'") + expect(entry).not.toContain('viewGrid(') }) }) diff --git a/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts b/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts index c193fd01..23a5cd1c 100644 --- a/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts +++ b/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts @@ -1,5 +1,2 @@ -export { - focusContextDetailDefinition, - focusContextOverviewDefinition, - mount, -} from './view' +export * from './example' +export { mount } from './view' diff --git a/benchmarks/conformance/cases/83-focus-context-window/view.tsx b/benchmarks/conformance/cases/83-focus-context-window/view.tsx index f3ab039f..ab135231 100644 --- a/benchmarks/conformance/cases/83-focus-context-window/view.tsx +++ b/benchmarks/conformance/cases/83-focus-context-window/view.tsx @@ -1,3 +1,7 @@ +import { + focusContextDetailDefinition, + focusContextOverviewDefinition, +} from './example' import { forwardRef, useCallback, @@ -6,19 +10,13 @@ import { useRef, useState, } from 'react' -import { defineChart, dot, lineY } from '@tanstack/charts' -import { brushX } from '@tanstack/charts/interaction/brush' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { keyedSelection, whenSelected } from '@tanstack/charts/selection' import { Chart } from '@tanstack/charts/react' import { aapl } from '@charts-poc/demo-data/aapl' -import { scaleLinear, scaleUtc } from 'd3-scale' import { catalogPreviewDefinition } from '../../shared/preview' import { reactMount } from '../../shared/react-mount' import { dateFromAnchor, dateKey, - focusContextDomain, initialFocusContextWindow, monthlyAaplRows, rowsInWindow, @@ -29,7 +27,6 @@ import type { BrushRange, BrushXChange, } from '@tanstack/charts/interaction/brush' -import type { KeyedSelectionChange } from '@tanstack/charts/selection' import type { AaplRow } from '@charts-poc/demo-data/aapl' import type { FocusContextWindow } from './model' import type { ConformanceTarget, ConformanceTestDriver } from '../../types' @@ -40,123 +37,9 @@ interface BrushStatus { outcome: 'idle' | 'dragging' | 'commit' | 'cancel' } -const detailMargin = { top: 16, right: 24, bottom: 38, left: 52 } -const overviewMargin = { top: 8, right: 24, bottom: 22, left: 52 } const gap = 8 const focusContextRows = monthlyAaplRows(aapl) const focusContextDates = focusContextRows.map((row) => row.Date) -const fullDomain = focusContextDomain(focusContextRows) - -export function focusContextDetailDefinition(window: FocusContextWindow) { - const rows = rowsInWindow(focusContextRows, window) - const selection = keyedSelection({ - selected: controlledSignal< - string | null, - KeyedSelectionChange - >(dateKey(window.selected), () => {}), - key: (row) => dateKey(row.Date), - }) - return defineChart( - defineChart({ - marks: [ - lineY(rows, { - id: 'detail-line', - x: 'Date', - y: 'Close', - stroke: '#2563eb', - strokeWidth: 2.5, - }), - dot(rows, { - id: 'detail-points', - x: 'Date', - y: 'Close', - fill: '#2563eb', - r: 3, - }), - whenSelected( - dot(rows, { - id: 'selected-point', - x: 'Date', - y: 'Close', - fill: '#f97316', - stroke: '#ffffff', - strokeWidth: 2, - r: 6, - }), - selection, - ), - ], - x: { - scale: scaleUtc().domain([window.start, window.end]), - axis: { label: 'Selected time window' }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { label: 'Close ($)' }, - }, - margin: detailMargin, - }), - { svgAnimation: false, keyboard: false }, - ) -} - -export function focusContextOverviewDefinition( - window: FocusContextWindow, - onChange: (range: BrushRange, reason: BrushXChange) => void, -) { - return defineChart( - defineChart({ - marks: [ - lineY(focusContextRows, { - id: 'overview-line', - x: 'Date', - y: 'Close', - stroke: '#2563eb', - strokeWidth: 1.75, - }), - ], - x: { - scale: scaleUtc().domain(fullDomain), - axis: { - ticks: { - count: 4, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - timeZone: 'UTC', - }), - }, - }, - }, - y: { scale: scaleLinear, axis: false }, - margin: overviewMargin, - controls: [ - brushX({ - id: 'focus-window', - range: controlledSignal, BrushXChange>( - { start: window.start, end: window.end }, - (next, { reason }) => onChange(next, reason), - ), - values: focusContextDates, - ariaLabel: 'Selected time window', - startAriaLabel: 'Selected time window start', - endAriaLabel: 'Selected time window end', - format: monthLabel, - handleSize: 16, - selectionStyle: { - fill: '#2563eb', - fillOpacity: 0.16, - stroke: '#2563eb', - strokeWidth: 1.5, - }, - handleStyle: { fill: '#2563eb', fillOpacity: 0.9 }, - }), - ], - }), - { svgAnimation: false, keyboard: false }, - ) -} const FocusContextExample = forwardRef< ConformanceTestDriver, diff --git a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/example.tsx b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/example.tsx new file mode 100644 index 00000000..8961b6a3 --- /dev/null +++ b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/example.tsx @@ -0,0 +1,427 @@ +import { useMemo, useRef } from 'react' +import { + areaY, + barX, + barY, + d3Curve, + defineChart, + dot, + lineY, + ruleX, + tickY, + whenFocused, +} from '@tanstack/charts' +import { renderChartSvgWithResources } from '@tanstack/charts/svg/resources' +import { Chart as NestedChart } from '@tanstack/charts/react' +import { Chart as TooltipChart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleBand, scaleLinear } from 'd3-scale' +import { curveMonotoneX } from 'd3-shape' +import { + consumptionBreakdown, + energyAnnualOverview, + energyColors, + energyMonths, + energyTooltipContent, + formatEnergy, +} from './model' +import { EnergyTooltipBody, energyTooltipStyles } from './tooltip-body' +import type { ChartScene } from '@tanstack/charts' +import type { EnergyMonth, EnergyMonthId } from './model' + +export function energyDefinition( + rows: readonly EnergyMonth[], + chartWidth: number, +) { + const months = rows.map((row) => row.monthShort) + const tooltipPlacement = + chartWidth >= 500 + ? ('right' as const) + : (['right', 'left', 'top', 'bottom'] as const) + return defineChart( + defineChart({ + marks: [ + areaY(rows, { + id: 'consumption-area', + x: 'monthShort', + y: 'consumption', + fill: energyColors.consumption, + fillOpacity: 0.13, + curve: d3Curve(curveMonotoneX), + }), + barY(rows, { + id: 'used-on-site', + x: 'monthShort', + y: 'usedOnSite', + fill: energyColors.generationMuted, + inset: 0.5, + states: [ + { + when: { focus: 'group' }, + style: { fill: energyColors.generation }, + transition: { + type: 'tween', + duration: 120, + easing: 'ease-out', + }, + }, + ], + }), + barY(rows, { + id: 'exported', + x: 'monthShort', + y1: 'usedOnSite', + y2: 'generation', + fill: 'url(#energy-exported-hatch)', + inset: 0.5, + radius: 3, + }), + lineY(rows, { + id: 'consumption-line', + x: 'monthShort', + y: 'consumption', + stroke: energyColors.consumption, + strokeWidth: 1.6, + curve: d3Curve(curveMonotoneX), + }), + whenFocused( + ruleX(rows, { + id: 'focused-month-guide', + x: 'monthShort', + stroke: 'CanvasText', + strokeOpacity: 0.45, + strokeWidth: 1, + strokeDasharray: '4 4', + }), + { match: 'x' }, + ), + whenFocused( + tickY(rows, { + id: 'focused-month-axis-marker', + x: 'monthShort', + y: () => 0, + stroke: 'CanvasText', + strokeWidth: 1.5, + }), + { match: 'x' }, + ), + dot(rows, { + id: 'consumption-points', + x: 'monthShort', + y: 'consumption', + fill: 'Canvas', + fillOpacity: 0, + stroke: energyColors.consumption, + strokeOpacity: 0, + strokeWidth: 1.4, + r: 4, + states: [ + { + when: { focus: 'group' }, + style: { + r: 4, + fillOpacity: 1, + strokeOpacity: 1, + strokeWidth: 1.8, + }, + transition: { + type: 'tween', + duration: 150, + easing: 'ease-out', + }, + }, + { + when: { focus: 'primary', pinned: true }, + style: { + r: 4.5, + fillOpacity: 1, + strokeOpacity: 1, + strokeWidth: 2, + }, + transition: { + type: 'tween', + duration: 180, + easing: 'ease-out', + }, + }, + ], + }), + ], + x: { + scale: scaleBand() + .domain(months) + .paddingInner(0.16) + .paddingOuter(0.06), + axis: { + line: false, + ticks: { size: 0, padding: 8 }, + }, + }, + y: { + scale: scaleLinear().domain([0, 2600]), + grid: true, + axis: { + line: false, + ticks: { + values: [0, 650, 1300, 1950, 2600], + size: 0, + padding: 7, + format: (value) => `${value.toLocaleString('en-US')} kWh`, + }, + }, + }, + margin: { top: 82, right: 24, bottom: 38, left: 72 }, + gradients: [ + { + id: 'energy-exported-hatch', + x1: 0, + y1: 0, + x2: 1, + y2: 1, + stops: exportedHatchStops, + }, + ], + }), + { + svgAnimation: false, + keyboard: true, + focus: 'group-x', + focusRing: false, + tooltip: { + use: tooltip, + className: 'energy-tooltip-surface', + anchor: 'point', + placement: tooltipPlacement, + offset: 12, + content: (points, { pinned }) => energyTooltipContent(points, pinned), + }, + }, + ) +} + +export function ConsumptionMixChart({ + month, + idPrefix, + catalogPreview = false, +}: { + readonly month: EnergyMonth + readonly idPrefix?: string + readonly catalogPreview?: boolean +}) { + const definition = useMemo(() => { + const parts = consumptionBreakdown(month) + return defineChart( + defineChart({ + marks: [ + barX(parts, { + id: 'consumption-breakdown', + x1: 'start', + x2: 'end', + y: () => 'mix', + fill: (part) => part.color, + inset: 0, + }), + ], + x: { + scale: scaleLinear().domain([0, month.consumption]), + axis: false, + }, + y: { + scale: scaleBand().domain(['mix']), + axis: false, + }, + margin: 0, + }), + { svgAnimation: false, keyboard: false, tooltip: false }, + ) + }, [month]) + + return ( + + ) +} + +export function AnnualMetric({ + label, + value, +}: { + label: string + value: string +}) { + const [amount, unit] = value.split(' ') + return ( +
+ + {label} + + + {amount}{' '} + + {unit} + + +
+ ) +} + +export const exportedHatchStops = Array.from({ length: 14 }, (_, index) => { + const start = index / 14 + const lineStart = (index + 0.82) / 14 + const end = (index + 1) / 14 + return [ + { offset: start, color: energyColors.exported }, + { offset: lineStart, color: energyColors.exported }, + { offset: lineStart, color: '#fff7e8' }, + { offset: end, color: '#fff7e8' }, + ] +}).flat() + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function EnergyTooltipExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '84-pinned-nested-chart-tooltip' + const viewRef = useRef(null) + + const focusedIdRef = useRef(null) + + const renderedRef = useRef<{ + scene: ChartScene + svg: SVGSVGElement + } | null>(null) + + const rows = useMemo(() => energyMonths(input.revision), [input.revision]) + + const chartWidth = Math.max(1, input.width - 24) + + const chartHeight = Math.max(1, input.height - 48) + + const annualConsumption = rows.reduce( + (total, month) => total + month.consumption, + 0, + ) + + const mainDefinition = useMemo( + () => energyDefinition(rows, chartWidth), + [chartWidth, rows], + ) + + return ( +
+ +
+ + Annual overview + +
+
+ + { + focusedIdRef.current = points[0]?.datum.id ?? null + }} + onRender={({ scene, svg }) => { + renderedRef.current = { scene, svg } + }} + renderTooltipBody={({ points, defaultBody, pinned, dismiss }) => { + const month = points[0]?.datum + if (!month) return defaultBody + return ( + + } + /> + ) + }} + /> +
+
+ ) +} diff --git a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/model.ts b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/model.ts index 02557dec..838ba082 100644 --- a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/model.ts +++ b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/model.ts @@ -1,5 +1,4 @@ import type { ChartTooltipContent } from '@tanstack/charts' -import type { ConformanceTarget } from '../../types' export const energyColors = { consumption: '#8b8d90', @@ -146,7 +145,7 @@ export function isEnergyMonthId(value: unknown): value is EnergyMonthId { return energyMonthIds.some((id) => id === value) } -export function monthFromTarget(target: ConformanceTarget) { +export function monthFromTarget(target: { view?: string; anchor: string }) { if (target.view !== undefined && target.view !== 'main') return null const [kind, id] = target.anchor.split(':') return kind === 'month' && isEnergyMonthId(id) ? id : null diff --git a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts index f702c6d6..23a5cd1c 100644 --- a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts @@ -1 +1,2 @@ +export * from './example' export { mount } from './view' diff --git a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/view.tsx b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/view.tsx index 035d6f8b..a6392d7f 100644 --- a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/view.tsx +++ b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/view.tsx @@ -1,30 +1,13 @@ +import { energyDefinition, ConsumptionMixChart, AnnualMetric } from './example' import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' -import { - areaY, - barX, - barY, - d3Curve, - defineChart, - dot, - lineY, - ruleX, - tickY, - whenFocused, -} from '@tanstack/charts' import { renderChartSvgWithResources } from '@tanstack/charts/svg/resources' -import { Chart as NestedChart } from '@tanstack/charts/react' import { Chart as TooltipChart } from '@tanstack/charts/react/tooltip' import { tooltip } from '@tanstack/charts/tooltip' -import { scaleBand, scaleLinear } from 'd3-scale' -import { curveMonotoneX } from 'd3-shape' import { catalogPreviewDefinition } from '../../shared/preview' import { reactMount } from '../../shared/react-mount' import { - consumptionBreakdown, energyAnnualOverview, - energyColors, energyMonths, - energyTooltipContent, formatEnergy, monthFromTarget, } from './model' @@ -259,222 +242,6 @@ const EnergyTooltipExample = forwardRef< export const catalogComponent = EnergyTooltipExample export const mount = reactMount(EnergyTooltipExample) -function energyDefinition(rows: readonly EnergyMonth[], chartWidth: number) { - const months = rows.map((row) => row.monthShort) - const tooltipPlacement = - chartWidth >= 500 - ? ('right' as const) - : (['right', 'left', 'top', 'bottom'] as const) - return defineChart( - defineChart({ - marks: [ - areaY(rows, { - id: 'consumption-area', - x: 'monthShort', - y: 'consumption', - fill: energyColors.consumption, - fillOpacity: 0.13, - curve: d3Curve(curveMonotoneX), - }), - barY(rows, { - id: 'used-on-site', - x: 'monthShort', - y: 'usedOnSite', - fill: energyColors.generationMuted, - inset: 0.5, - states: [ - { - when: { focus: 'group' }, - style: { fill: energyColors.generation }, - transition: { - type: 'tween', - duration: 120, - easing: 'ease-out', - }, - }, - ], - }), - barY(rows, { - id: 'exported', - x: 'monthShort', - y1: 'usedOnSite', - y2: 'generation', - fill: 'url(#energy-exported-hatch)', - inset: 0.5, - radius: 3, - }), - lineY(rows, { - id: 'consumption-line', - x: 'monthShort', - y: 'consumption', - stroke: energyColors.consumption, - strokeWidth: 1.6, - curve: d3Curve(curveMonotoneX), - }), - whenFocused( - ruleX(rows, { - id: 'focused-month-guide', - x: 'monthShort', - stroke: 'CanvasText', - strokeOpacity: 0.45, - strokeWidth: 1, - strokeDasharray: '4 4', - }), - { match: 'x' }, - ), - whenFocused( - tickY(rows, { - id: 'focused-month-axis-marker', - x: 'monthShort', - y: () => 0, - stroke: 'CanvasText', - strokeWidth: 1.5, - }), - { match: 'x' }, - ), - dot(rows, { - id: 'consumption-points', - x: 'monthShort', - y: 'consumption', - fill: 'Canvas', - fillOpacity: 0, - stroke: energyColors.consumption, - strokeOpacity: 0, - strokeWidth: 1.4, - r: 4, - states: [ - { - when: { focus: 'group' }, - style: { - r: 4, - fillOpacity: 1, - strokeOpacity: 1, - strokeWidth: 1.8, - }, - transition: { - type: 'tween', - duration: 150, - easing: 'ease-out', - }, - }, - { - when: { focus: 'primary', pinned: true }, - style: { - r: 4.5, - fillOpacity: 1, - strokeOpacity: 1, - strokeWidth: 2, - }, - transition: { - type: 'tween', - duration: 180, - easing: 'ease-out', - }, - }, - ], - }), - ], - x: { - scale: scaleBand() - .domain(months) - .paddingInner(0.16) - .paddingOuter(0.06), - axis: { - line: false, - ticks: { size: 0, padding: 8 }, - }, - }, - y: { - scale: scaleLinear().domain([0, 2600]), - grid: true, - axis: { - line: false, - ticks: { - values: [0, 650, 1300, 1950, 2600], - size: 0, - padding: 7, - format: (value) => `${value.toLocaleString('en-US')} kWh`, - }, - }, - }, - margin: { top: 82, right: 24, bottom: 38, left: 72 }, - gradients: [ - { - id: 'energy-exported-hatch', - x1: 0, - y1: 0, - x2: 1, - y2: 1, - stops: exportedHatchStops, - }, - ], - }), - { - svgAnimation: false, - keyboard: true, - focus: 'group-x', - focusRing: false, - tooltip: { - use: tooltip, - className: 'energy-tooltip-surface', - anchor: 'point', - placement: tooltipPlacement, - offset: 12, - content: (points, { pinned }) => energyTooltipContent(points, pinned), - }, - }, - ) -} - -function ConsumptionMixChart({ - month, - idPrefix, - catalogPreview = false, -}: { - readonly month: EnergyMonth - readonly idPrefix?: string - readonly catalogPreview?: boolean -}) { - const definition = useMemo(() => { - const parts = consumptionBreakdown(month) - return defineChart( - defineChart({ - marks: [ - barX(parts, { - id: 'consumption-breakdown', - x1: 'start', - x2: 'end', - y: () => 'mix', - fill: (part) => part.color, - inset: 0, - }), - ], - x: { - scale: scaleLinear().domain([0, month.consumption]), - axis: false, - }, - y: { - scale: scaleBand().domain(['mix']), - axis: false, - }, - margin: 0, - }), - { svgAnimation: false, keyboard: false, tooltip: false }, - ) - }, [month]) - - return ( - - ) -} - function pointCoordinate( rendered: { scene: ChartScene @@ -496,39 +263,6 @@ function pointCoordinate( } } -function AnnualMetric({ label, value }: { label: string; value: string }) { - const [amount, unit] = value.split(' ') - return ( -
- - {label} - - - {amount}{' '} - - {unit} - - -
- ) -} - -const exportedHatchStops = Array.from({ length: 14 }, (_, index) => { - const start = index / 14 - const lineStart = (index + 0.82) / 14 - const end = (index + 1) / 14 - return [ - { offset: start, color: energyColors.exported }, - { offset: lineStart, color: energyColors.exported }, - { offset: lineStart, color: '#fff7e8' }, - { offset: end, color: '#fff7e8' }, - ] -}).flat() - function center(element: HTMLElement | SVGElement) { const bounds = element.getBoundingClientRect() return { diff --git a/benchmarks/conformance/cases/85-scrollable-resource-lanes/example.tsx b/benchmarks/conformance/cases/85-scrollable-resource-lanes/example.tsx new file mode 100644 index 00000000..d49810da --- /dev/null +++ b/benchmarks/conformance/cases/85-scrollable-resource-lanes/example.tsx @@ -0,0 +1,377 @@ +import { useLayoutEffect, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import { timelineStatusColors } from './colors' +import { + timelineBodyHeight, + timelineChartHeight, + timelineContentWidth, + timelineLaneRailWidth, +} from './layout' +import { resourceLanes, resourceTasks, timelineStatuses } from './scenario' +import type { ChartPoint, ChartScene } from '@tanstack/charts' +import type { ResourceLane, ResourceTask } from './scenario' +import { defineChart, rect } from '@tanstack/charts' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleBand, scaleUtc } from 'd3-scale' +import { timelineMargin } from './layout' +import { resourceTimelineDomain } from './scenario' +const focusScrollPadding = 32 + +interface TimelineFocusState { + taskId: string | null + centerX: number | null + scrolled: boolean +} + +const visuallyHidden = { + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + border: 0, +} as const + +function ensureTimelineFocusVisible(viewport: HTMLDivElement, centerX: number) { + const previous = viewport.scrollLeft + const visibleStart = previous + focusScrollPadding + const visibleEnd = previous + viewport.clientWidth - focusScrollPadding + let next = previous + if (centerX < visibleStart) next = centerX - focusScrollPadding + else if (centerX > visibleEnd) { + next = centerX - viewport.clientWidth + focusScrollPadding + } + viewport.scrollLeft = Math.max( + 0, + Math.min(next, viewport.scrollWidth - viewport.clientWidth), + ) + return Math.abs(viewport.scrollLeft - previous) > 1 +} + +function taskDetails(task: ResourceTask) { + return `${task.resource} · ${task.label} · ${task.status} · ${formatDate(task.start)}–${formatDate(task.end)}` +} + +function formatDate(date: Date) { + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', + }) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const taskInset = 5 + +export const resourceTimelineDefinition = (input: ExampleChartInput) => { + const rows = resourceTasks(input.revision) + + return defineChart( + defineChart(({ width }) => { + return { + marks: [ + rect(rows, { + key: 'id', + x1: 'start', + x2: 'end', + y: 'resource', + color: 'status', + inset: taskInset, + radius: 4, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleUtc().domain(resourceTimelineDomain), + grid: true, + axis: { ticks: { count: Math.max(6, Math.floor(width / 84)) } }, + }, + y: { + scale: scaleBand() + .domain(resourceLanes) + .paddingInner(0.08) + .paddingOuter(0.04), + grid: false, + axis: false, + }, + color: { + domain: timelineStatuses, + range: timelineStatuses.map((status) => timelineStatusColors[status]), + }, + margin: timelineMargin, + } + }), + { + svgAnimation: false, + keyboard: true, + tooltip: { + use: tooltip, + format: (point) => + `${point.datum.resource} · ${point.datum.label} · ${ + point.datum.status + } · ${formatTaskDate(point.datum.start)}–${formatTaskDate( + point.datum.end, + )}`, + }, + }, + ) +} + +function formatTaskDate(date: Date) { + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export default function ResourceTimelineExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '85-scrollable-resource-lanes' + const viewportRef = useRef(null) + + const chartSurfaceRef = useRef(null) + + const sceneRef = useRef>(null) + + const inputRef = useRef(input) + + const focusRef = useRef({ + taskId: null, + centerX: null, + scrolled: false, + }) + + const [focusedTask, setFocusedTask] = useState(null) + + const [lanePositions, setLanePositions] = useState< + Readonly> + >({} as Record) + + inputRef.current = input + + const rows = useMemo(() => resourceTasks(input.revision), [input.revision]) + + const definition = useMemo(() => resourceTimelineDefinition(input), [input]) + + const railWidth = timelineLaneRailWidth(input.width) + + const bodyHeight = timelineBodyHeight(input.height) + + const viewportWidth = Math.max(1, input.width - railWidth) + + const contentWidth = timelineContentWidth(viewportWidth) + + const chartHeight = timelineChartHeight(bodyHeight) + + useLayoutEffect(() => { + const viewport = viewportRef.current + if (!viewport) return + viewport.scrollLeft = Math.min( + viewport.scrollLeft, + Math.max(0, viewport.scrollWidth - viewport.clientWidth), + ) + }, [contentWidth, input.revision, viewportWidth]) + + const updateFocusedTask = (point: ChartPoint | null) => { + const viewport = viewportRef.current + const chartSurface = chartSurfaceRef.current + const keyboardFocused = + chartSurface?.querySelector('svg.ts-chart') === + chartSurface?.ownerDocument.activeElement + focusRef.current = { + taskId: point?.datum.id ?? null, + centerX: point?.x ?? null, + scrolled: + viewport && point && keyboardFocused + ? ensureTimelineFocusVisible(viewport, point.x) + : false, + } + setFocusedTask(point?.datum ?? null) + } + + return ( +
+
+ {timelineStatuses.map((status) => ( + + + {status[0]?.toUpperCase() + status.slice(1)} + + ))} + + {focusedTask ? taskDetails(focusedTask) : 'Scroll dates →'} + +
+
+
+ {resourceLanes.map((lane) => ( + + {lane} + + ))} +
+
+
+ { + sceneRef.current = scene + const next = Object.fromEntries( + resourceLanes.map((lane) => [lane, scene.scales.y.map(lane)]), + ) as Record + setLanePositions((current) => + resourceLanes.every((lane) => current[lane] === next[lane]) + ? current + : next, + ) + }} + /> +
+
+
+
    + {rows.map((row) => ( +
  • + {row.resource}: {row.label}, {row.status}, {formatDate(row.start)}{' '} + through {formatDate(row.end)} +
  • + ))} +
+
+ ) +} diff --git a/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts b/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts index 262c4716..5f2bb522 100644 --- a/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts +++ b/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts @@ -1,76 +1,11 @@ -import { defineChart, rect } from '@tanstack/charts' -import { tooltip } from '@tanstack/charts/tooltip' -import { scaleBand, scaleUtc } from 'd3-scale' -import { timelineStatusColors } from './colors' -import { timelineMargin } from './layout' -import { - resourceLanes, - resourceTasks, - resourceTimelineDomain, - timelineStatuses, -} from './scenario' +import { resourceTimelineDefinition } from './example' +export { resourceTimelineDefinition } from './example' import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' -const taskInset = 5 +export { default as Example } from './example' export { mount } from './view' -export const resourceTimelineDefinition = (input: ConformanceInput) => { - const rows = resourceTasks(input.revision) - - return defineChart( - defineChart(({ width }) => { - return { - marks: [ - rect(rows, { - key: 'id', - x1: 'start', - x2: 'end', - y: 'resource', - color: 'status', - inset: taskInset, - radius: 4, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - x: { - scale: scaleUtc().domain(resourceTimelineDomain), - grid: true, - axis: { ticks: { count: Math.max(6, Math.floor(width / 84)) } }, - }, - y: { - scale: scaleBand() - .domain(resourceLanes) - .paddingInner(0.08) - .paddingOuter(0.04), - grid: false, - axis: false, - }, - color: { - domain: timelineStatuses, - range: timelineStatuses.map((status) => timelineStatusColors[status]), - }, - margin: timelineMargin, - } - }), - { - svgAnimation: false, - keyboard: true, - tooltip: { - use: tooltip, - format: (point) => - `${point.datum.resource} · ${point.datum.label} · ${ - point.datum.status - } · ${formatTaskDate(point.datum.start)}–${formatTaskDate( - point.datum.end, - )}`, - }, - }, - ) -} - export const catalogCase = tanstackCase( resourceTimelineDefinition, 'Tasks scheduled across five resource lanes', diff --git a/benchmarks/conformance/cases/85-scrollable-resource-lanes/view.tsx b/benchmarks/conformance/cases/85-scrollable-resource-lanes/view.tsx index 9445d061..036b6bb4 100644 --- a/benchmarks/conformance/cases/85-scrollable-resource-lanes/view.tsx +++ b/benchmarks/conformance/cases/85-scrollable-resource-lanes/view.tsx @@ -22,7 +22,7 @@ import { timelineStatuses, } from './scenario' import { timelineDateKey } from './model' -import { resourceTimelineDefinition } from './tanstack' +import { resourceTimelineDefinition } from './example' import type { ChartPoint, ChartScene } from '@tanstack/charts' import type { ResourceLane, ResourceTask } from './scenario' import type { diff --git a/benchmarks/conformance/cases/86-streaming-window-preservation/example.tsx b/benchmarks/conformance/cases/86-streaming-window-preservation/example.tsx new file mode 100644 index 00000000..45823252 --- /dev/null +++ b/benchmarks/conformance/cases/86-streaming-window-preservation/example.tsx @@ -0,0 +1,288 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { decorative } from '@tanstack/charts/mark/decorative' +import { tooltip } from '@tanstack/charts/tooltip' +import { Chart } from '@tanstack/charts/react' +import { downloads } from '@charts-poc/demo-data/downloads' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { streamingData } from './selection' +import { + formatStreamingDate, + streamingStatus, + streamingViewportForMode, + streamingViewportLabel, + visibleStreamingData, +} from './model' +import type { ChartScene } from '@tanstack/charts' +import type { DownloadsRow } from '@charts-poc/demo-data/downloads' +import type { StreamingViewportMode } from './model' + +const color = '#2563eb' + +export function streamingWindowDefinition( + rows: readonly DownloadsRow[], + viewport: readonly [Date, Date], + viewportMode: StreamingViewportMode, +) { + const visibleRows = visibleStreamingData(rows, viewport) + return defineChart({ + marks: [ + decorative( + lineY(visibleRows, { + id: 'stream-line', + x: 'date', + y: 'downloads', + stroke: color, + strokeWidth: 2.5, + }), + ), + dot(visibleRows, { + id: 'stream-points', + x: 'date', + y: 'downloads', + fill: color, + r: 3.5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleUtc().domain(viewport), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + label: streamingViewportLabel(viewportMode), + }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 5 }, label: 'Downloads' }, + }, + margin: { top: 18, right: 24, bottom: 44, left: 58 }, + svgAnimation: false, + keyboard: true, + tooltip: { + use: tooltip, + format: (point) => + `${formatStreamingDate(point.datum.date)} · ${point.datum.downloads.toLocaleString()} downloads`, + }, + }) +} + +function ControlButton({ + children, + control, + label, + onClick, + pressed, +}: { + children: string + control: 'append' | 'follow' | 'all' + label: string + onClick: () => void + pressed?: boolean +}) { + return ( + + ) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export default function StreamingExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '86-streaming-window-preservation' + const viewRef = useRef(null) + + const chartSurfaceRef = useRef(null) + + const sceneRef = useRef | null>(null) + + const [appended, setAppended] = useState(0) + + const [viewportMode, setViewportMode] = + useState('locked') + + const [announcement, setAnnouncement] = useState('') + + const rows = useMemo( + () => streamingData(downloads, input.revision, appended), + [appended, input.revision], + ) + + const viewport = useMemo( + () => streamingViewportForMode(rows, viewportMode), + [rows, viewportMode], + ) + + const stateRef = useRef({ rows, appended, viewport, viewportMode }) + + stateRef.current = { rows, appended, viewport, viewportMode } + + const chartHeight = Math.max(180, input.height - 78) + + const definition = useMemo( + () => streamingWindowDefinition(rows, viewport, viewportMode), + [rows, viewport, viewportMode], + ) + + const status = streamingStatus({ + rows, + viewport, + viewportMode, + announcement, + }) + + useEffect(() => { + setAnnouncement('') + }, [input.revision]) + + const append = () => { + const nextAppended = appended + 1 + const nextRows = streamingData(downloads, input.revision, nextAppended) + const nextViewport = streamingViewportForMode(nextRows, viewportMode) + const added = nextRows.at(-1) + setAppended(nextAppended) + setAnnouncement( + added + ? `Added ${formatStreamingDate(added.date)} (${added.downloads.toLocaleString()} downloads). ${ + visibleStreamingData([added], nextViewport).length + ? 'The new sample is visible.' + : `It is outside the locked viewport ending ${formatStreamingDate(nextViewport[1])}.` + }` + : '', + ) + } + + return ( +
+
+ + Append + + { + setViewportMode('latest') + const nextViewport = streamingViewportForMode(rows, 'latest') + setAnnouncement( + `Following the latest samples through ${formatStreamingDate(nextViewport[1])}.`, + ) + }} + > + Follow latest + + { + setViewportMode('all') + setAnnouncement( + `Viewport unlocked. Showing all ${rows.length} samples.`, + ) + }} + > + Show all + + + {status} + +
+
+ { + sceneRef.current = scene + }} + /> +
+
+ ) +} diff --git a/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.test.ts b/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.test.ts index 9d4b0714..ae3932c4 100644 --- a/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.test.ts +++ b/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.test.ts @@ -130,10 +130,12 @@ describe('definition-owned streaming window', () => { 'benchmarks/conformance/cases/86-streaming-window-preservation', ) const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') + const example = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const echarts = readFileSync(resolve(directory, 'echarts.ts'), 'utf8') + expect(view).toContain("from './example'") expect(view).toContain('streamingWindowDefinition(') - expect(view).toContain('decorative(') + expect(example).toContain('decorative(') expect(view).not.toContain(' - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), - }, - label: streamingViewportLabel(viewportMode), - }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 5 }, label: 'Downloads' }, - }, - margin: { top: 18, right: 24, bottom: 44, left: 58 }, - svgAnimation: false, - keyboard: true, - tooltip: { - use: tooltip, - format: (point) => - `${formatStreamingDate(point.datum.date)} · ${point.datum.downloads.toLocaleString()} downloads`, - }, - }) -} - const StreamingExample = forwardRef< ConformanceTestDriver, ReactConformanceProps diff --git a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/example.tsx b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/example.tsx new file mode 100644 index 00000000..67132a00 --- /dev/null +++ b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/example.tsx @@ -0,0 +1,394 @@ +import { useMemo, useRef, useState } from 'react' +import { travelers } from '@charts-poc/demo-data/travelers' +import { Chart } from '@tanstack/charts/react' +import { synchronizedCursorColors } from './colors' +import { + synchronizedCursorDateKey, + synchronizedCursorDatumAtDate, +} from './model' +import { selectSynchronizedCursorData } from './selection' +import type { ChartScene } from '@tanstack/charts' +import type { ChartInteractionController } from '@tanstack/charts' +import type { TravelersRow } from '@charts-poc/demo-data/travelers' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { focusGuideX } from '@tanstack/charts/focus/guide' +import { decorative } from '@tanstack/charts/mark/decorative' +import { tooltip } from '@tanstack/charts/tooltip' +import { viewGrid } from '@tanstack/charts/view' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { synchronizedCursorViews, synchronizedCursorYDomains } from './model' +import type { ChartTooltipOptions } from '@tanstack/charts' +import type { SynchronizedCursorView } from './model' +function SummaryValue({ + color, + dataAttribute, + label, + value, +}: { + color: string + dataAttribute: 'date' | 'current' | 'previous' + label: string + value: string +}) { + return ( + + ) +} + +function formatDate(date: Date) { + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + timeZone: 'UTC', + }) +} + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export const summaryHeight = 56 + +const viewGap = 8 + +const viewMargin = { top: 16, right: 24, bottom: 34, left: 62 } as const + +const travelerCountFormat = new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, +}) + +const month = new Intl.DateTimeFormat('en-US', { + month: 'short', + timeZone: 'UTC', +}) + +const synchronizedCursorTooltip: ChartTooltipOptions = { + sticky: true, + visibility: 'pinned', + anchor: 'point', + placement: ['bottom-right', 'bottom-left', 'right', 'left'], + offset: 10, + formatGroup: () => 'Pinned · Press Escape to release', +} + +export const synchronizedCursorDefinition = (input: ExampleChartInput) => { + const rows = selectSynchronizedCursorData(travelers, input.revision) + const composed = viewGrid({ + id: 'synchronized-cursors', + rows: synchronizedCursorViews.map((view) => ({ id: view, grow: 1 })), + columns: [{ id: 'main', grow: 1 }], + rowGap: input.preview ? 4 : viewGap, + views: synchronizedCursorViews.map((view) => ({ + id: view, + row: view, + column: 'main' as const, + ...(view === 'previous' ? { share: { x: 'current' as const } } : {}), + chart: synchronizedCursorViewDefinition( + rows, + view, + input.preview === true, + ), + })), + }) + + return defineChart(composed, { + svgAnimation: false, + keyboard: true, + focus: 'group-x', + focusRing: false, + maxFocusDistance: Number.POSITIVE_INFINITY, + tooltip: { + use: tooltip, + ...synchronizedCursorTooltip, + }, + }) +} + +function synchronizedCursorViewDefinition( + rows: readonly TravelersRow[], + view: SynchronizedCursorView, + preview: boolean, +) { + const group = () => view + return defineChart({ + marks: [ + decorative( + lineY(rows, { + id: `${view}-line`, + key: (row) => synchronizedCursorDateKey(row.date), + x: 'date', + y: view, + z: group, + stroke: synchronizedCursorColors[view], + strokeWidth: 2, + }), + ), + dot(rows, { + id: `${view}-points`, + key: (row) => synchronizedCursorDateKey(row.date), + x: 'date', + y: view, + z: group, + fill: synchronizedCursorColors[view], + r: 3, + stroke: '#ffffff', + strokeWidth: 1, + }), + focusGuideX(rows, { + id: `${view}-guide`, + key: (row) => synchronizedCursorDateKey(row.date), + x: 'date', + y: view, + z: group, + match: 'x', + xRule: { + stroke: '#64748b', + strokeWidth: 1, + strokeDasharray: '4 4', + }, + marker: { + radius: 5, + fill: '#ffffff', + stroke: '#334155', + strokeWidth: 2, + }, + }), + ], + x: { + scale: scaleUtc, + axis: preview + ? false + : { ticks: { format: (value) => month.format(value) } }, + }, + y: { + scale: scaleLinear().domain(synchronizedCursorYDomains[view]), + grid: !preview, + axis: preview + ? false + : { + ticks: { count: 4, format: travelerCountFormat.format }, + label: view === 'current' ? '2020 travelers' : '2019 travelers', + }, + }, + margin: preview ? 0 : viewMargin, + }) +} + +export function chartHeight(input: ExampleChartInput) { + return Math.max(280, input.height - summaryHeight) +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export default function SynchronizedCursorsExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '87-echarts-synchronized-cursors' + const chartFrameRef = useRef(null) + + const sceneRef = useRef>(null) + + const interactionRef = useRef< + ChartInteractionController | undefined + >(undefined) + + const inputRef = useRef(input) + + const previousInputRef = useRef(input) + + const restoreDateRef = useRef(undefined) + + const stateRef = useRef<{ date: Date | null; pinned: boolean }>({ + date: null, + pinned: false, + }) + + const [focusedDate, setFocusedDate] = useState(null) + + const [pinned, setPinned] = useState(false) + + if (previousInputRef.current !== input) { + restoreDateRef.current = stateRef.current.date + previousInputRef.current = input + } + + inputRef.current = input + + stateRef.current = { date: focusedDate, pinned } + + const definition = useMemo(() => synchronizedCursorDefinition(input), [input]) + + const rows = useMemo( + () => selectSynchronizedCursorData(travelers, input.revision), + [input.revision], + ) + + const row = focusedDate + ? synchronizedCursorDatumAtDate(rows, focusedDate) + : undefined + + return ( +
{ + if (stateRef.current.pinned) return + interactionRef.current?.setControlledFocus(null) + stateRef.current = { date: null, pinned: false } + setFocusedDate(null) + setPinned(false) + }} + style={{ + display: 'grid', + gridTemplateRows: `${summaryHeight}px minmax(0, 1fr)`, + width: input.width, + height: input.height, + }} + > +
+ + + +
+
+ { + const date = points[0]?.datum.date ?? null + setFocusedDate(date) + if (!date) setPinned(false) + }} + onSelect={(point) => { + if (!point) return + setFocusedDate(point.datum.date) + setPinned((value) => !value) + }} + onRender={({ interaction, scene }) => { + sceneRef.current = scene + interactionRef.current = interaction + const restoreDate = restoreDateRef.current + restoreDateRef.current = undefined + if (!restoreDate) return + const timestamp = restoreDate.getTime() + const point = scene.points.find( + (candidate) => + candidate.markId.endsWith(':current:current-points') && + candidate.datum.date.getTime() === timestamp, + ) + if (point) { + interaction.setControlledFocus(point, { + source: 'programmatic', + pinned: stateRef.current.pinned, + }) + } + }} + /> +
+
+ ) +} diff --git a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.test.ts b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.test.ts index 324fcb77..535689db 100644 --- a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.test.ts +++ b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.test.ts @@ -229,7 +229,7 @@ describe('definition-owned synchronized cursors', () => { process.cwd(), 'benchmarks/conformance/cases/87-echarts-synchronized-cursors', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') for (const forbidden of [ diff --git a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts index 030d9e36..33bfba52 100644 --- a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts +++ b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts @@ -1,10 +1,10 @@ +import { synchronizedCursorDefinition } from './example' +export { + summaryHeight, + synchronizedCursorDefinition, + chartHeight, +} from './example' import { travelers } from '@charts-poc/demo-data/travelers' -import { defineChart, dot, lineY } from '@tanstack/charts' -import { focusGuideX } from '@tanstack/charts/focus/guide' -import { decorative } from '@tanstack/charts/mark/decorative' -import { tooltip } from '@tanstack/charts/tooltip' -import { viewGrid } from '@tanstack/charts/view' -import { scaleLinear, scaleUtc } from 'd3-scale' import { clientPointBounds, scenePointToClient, @@ -16,8 +16,6 @@ import { synchronizedCursorDateKey, synchronizedCursorDatumAtDate, synchronizedCursorNearestDatum, - synchronizedCursorViews, - synchronizedCursorYDomains, } from './model' import { selectSynchronizedCursorData } from './selection' import type { @@ -37,20 +35,9 @@ import type { ConformanceTestDriver, } from '../../types' -export const summaryHeight = 56 -const viewGap = 8 +export { default as Example } from './example' const viewMargin = { top: 16, right: 24, bottom: 34, left: 62 } as const -const travelerCountFormat = new Intl.NumberFormat('en-US', { - notation: 'compact', - maximumFractionDigits: 1, -}) - -const month = new Intl.DateTimeFormat('en-US', { - month: 'short', - timeZone: 'UTC', -}) - const synchronizedCursorTooltip: ChartTooltipOptions = { sticky: true, visibility: 'pinned', @@ -60,109 +47,6 @@ const synchronizedCursorTooltip: ChartTooltipOptions = { formatGroup: () => 'Pinned · Press Escape to release', } -export const synchronizedCursorDefinition = (input: ConformanceInput) => { - const rows = selectSynchronizedCursorData(travelers, input.revision) - const composed = viewGrid({ - id: 'synchronized-cursors', - rows: synchronizedCursorViews.map((view) => ({ id: view, grow: 1 })), - columns: [{ id: 'main', grow: 1 }], - rowGap: input.preview ? 4 : viewGap, - views: synchronizedCursorViews.map((view) => ({ - id: view, - row: view, - column: 'main' as const, - ...(view === 'previous' ? { share: { x: 'current' as const } } : {}), - chart: synchronizedCursorViewDefinition( - rows, - view, - input.preview === true, - ), - })), - }) - - return defineChart(composed, { - svgAnimation: false, - keyboard: true, - focus: 'group-x', - focusRing: false, - maxFocusDistance: Number.POSITIVE_INFINITY, - tooltip: { - use: tooltip, - ...synchronizedCursorTooltip, - }, - }) -} - -function synchronizedCursorViewDefinition( - rows: readonly TravelersRow[], - view: SynchronizedCursorView, - preview: boolean, -) { - const group = () => view - return defineChart({ - marks: [ - decorative( - lineY(rows, { - id: `${view}-line`, - key: (row) => synchronizedCursorDateKey(row.date), - x: 'date', - y: view, - z: group, - stroke: synchronizedCursorColors[view], - strokeWidth: 2, - }), - ), - dot(rows, { - id: `${view}-points`, - key: (row) => synchronizedCursorDateKey(row.date), - x: 'date', - y: view, - z: group, - fill: synchronizedCursorColors[view], - r: 3, - stroke: '#ffffff', - strokeWidth: 1, - }), - focusGuideX(rows, { - id: `${view}-guide`, - key: (row) => synchronizedCursorDateKey(row.date), - x: 'date', - y: view, - z: group, - match: 'x', - xRule: { - stroke: '#64748b', - strokeWidth: 1, - strokeDasharray: '4 4', - }, - marker: { - radius: 5, - fill: '#ffffff', - stroke: '#334155', - strokeWidth: 2, - }, - }), - ], - x: { - scale: scaleUtc, - axis: preview - ? false - : { ticks: { format: (value) => month.format(value) } }, - }, - y: { - scale: scaleLinear().domain(synchronizedCursorYDomains[view]), - grid: !preview, - axis: preview - ? false - : { - ticks: { count: 4, format: travelerCountFormat.format }, - label: view === 'current' ? '2020 travelers' : '2019 travelers', - }, - }, - margin: preview ? 0 : viewMargin, - }) -} - export const catalogCase = tanstackCase( synchronizedCursorDefinition, 'Linked 2020 and 2019 airport traveler time series', @@ -386,7 +270,3 @@ function renderedCrosshairState( xNormalized: (x - viewBounds.x) / viewBounds.width, } } - -export function chartHeight(input: ConformanceInput) { - return Math.max(280, input.height - summaryHeight) -} diff --git a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/view.tsx b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/view.tsx index 5b6fee84..fa0813e6 100644 --- a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/view.tsx +++ b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/view.tsx @@ -14,12 +14,12 @@ import { synchronizedCursorDatumAtDate, } from './model' import { selectSynchronizedCursorData } from './selection' +import { createDriver } from './tanstack' import { chartHeight, - createDriver, summaryHeight, synchronizedCursorDefinition, -} from './tanstack' +} from './example' import type { ChartScene } from '@tanstack/charts' import type { ChartInteractionController } from '@tanstack/charts' import type { TravelersRow } from '@charts-poc/demo-data/travelers' diff --git a/benchmarks/conformance/cases/88-echarts-free-cursor/example.tsx b/benchmarks/conformance/cases/88-echarts-free-cursor/example.tsx new file mode 100644 index 00000000..6853d6a9 --- /dev/null +++ b/benchmarks/conformance/cases/88-echarts-free-cursor/example.tsx @@ -0,0 +1,430 @@ +import { forwardRef, memo, useCallback, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import type { KeyboardEvent } from 'react' +import { formatFreeCursorValue } from './format' +import { freeCursorXDomain, freeCursorYDomain } from './model' +import type { ChartScene } from '@tanstack/charts' +import type { + ContinuousCursorChange, + ContinuousCursorPosition, +} from '@tanstack/charts/interaction/cursor' +import type { CompleteCar } from './model' +import { cars } from '@charts-poc/demo-data/cars' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { continuousCursor } from '@tanstack/charts/interaction/cursor' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { decorative } from '@tanstack/charts/mark/decorative' +import { scaleLinear } from 'd3-scale' +import { freeCursorRows } from './model' +const CursorSlider = forwardRef< + HTMLInputElement, + { + ariaLabel: string + domain: readonly [number, number] + label: string + onChange: (value: number) => void + value: number + } +>(function CursorSlider({ ariaLabel, domain, label, onChange, value }, ref) { + return ( + + ) +}) + +const FreeCursorChart = memo(function FreeCursorChart({ + accepted, + idPrefix, + input, + onChange, + onRender, +}: { + accepted: ContinuousCursorPosition | null + idPrefix?: string + input: { width: number; height: number } + onChange: Parameters[1] + onRender: (scene: ChartScene) => void +}) { + const definition = useMemo( + () => freeCursorDefinition(accepted, onChange), + [accepted, onChange], + ) + return ( + onRender(scene)} + /> + ) +}) + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export interface CursorState { + visible: boolean + xNormalized: number | null + yNormalized: number | null + xValue: number | null + yValue: number | null + pinned: boolean +} + +export const cursorControlsHeight = 68 + +export function cursorState( + position: ContinuousCursorPosition, + pinned: boolean, +): CursorState { + const xValue = roundCursorValue(position.x) + const yValue = roundCursorValue(position.y) + return { + visible: true, + xNormalized: + (xValue - freeCursorXDomain[0]) / + (freeCursorXDomain[1] - freeCursorXDomain[0]), + yNormalized: + 1 - + (yValue - freeCursorYDomain[0]) / + (freeCursorYDomain[1] - freeCursorYDomain[0]), + xValue, + yValue, + pinned, + } +} + +export function clearedCursor(): CursorState { + return { + visible: false, + xNormalized: null, + yNormalized: null, + xValue: null, + yValue: null, + pinned: false, + } +} + +export function roundedPosition( + position: ContinuousCursorPosition, +): ContinuousCursorPosition { + return { + x: roundCursorValue(position.x), + y: roundCursorValue(position.y), + } +} + +function roundCursorValue(value: number) { + return Math.round(value * 10) / 10 +} + +export function chartHeight(height: number) { + return Math.max(180, height - cursorControlsHeight) +} + +const rows = freeCursorRows(cars) + +export function freeCursorDefinition( + position: ContinuousCursorPosition | null, + onChange: ( + value: ContinuousCursorPosition | null, + reason: ContinuousCursorChange, + ) => void, + preview = false, +) { + return defineChart({ + marks: [ + decorative( + lineY(rows, { + id: 'free-cursor-line', + x: 'power (hp)', + y: 'economy (mpg)', + stroke: '#0f766e', + strokeWidth: 2, + }), + ), + dot(rows, { + id: 'free-cursor-dots', + x: 'power (hp)', + y: 'economy (mpg)', + fill: '#0f766e', + r: 3.5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleLinear().domain(freeCursorXDomain), + axis: { label: 'Horsepower' }, + }, + y: { + scale: scaleLinear().domain(freeCursorYDomain), + grid: true, + axis: { ticks: { count: 7 }, label: 'Fuel economy (mpg)' }, + }, + controls: [ + continuousCursor({ + position: controlledSignal< + ContinuousCursorPosition | null, + ContinuousCursorChange + >(position, (next, { reason }) => onChange(next, reason)), + xRule: { + stroke: '#64748b', + strokeWidth: 1, + strokeDasharray: '4 4', + }, + yRule: { + stroke: '#64748b', + strokeWidth: 1, + strokeDasharray: '4 4', + }, + marker: { + radius: 4, + fill: '#ffffff', + stroke: '#0f766e', + strokeWidth: 2, + }, + xLabel: { + format: (value) => + formatFreeCursorValue('HP', roundCursorValue(value)), + ...(preview + ? { + offset: 2, + paddingX: 3, + paddingY: 2, + fontSize: 8, + color: 'Canvas', + background: 'CanvasText', + stroke: 'Canvas', + } + : {}), + }, + yLabel: { + side: 'start', + format: (value) => + formatFreeCursorValue('MPG', roundCursorValue(value)), + ...(preview + ? { + offset: 2, + paddingX: 3, + paddingY: 2, + fontSize: 8, + color: 'Canvas', + background: 'CanvasText', + stroke: 'Canvas', + } + : {}), + }, + }), + ], + svgAnimation: false, + keyboard: false, + focusRing: false, + margin: preview + ? { top: 0, right: 0, bottom: 14, left: 40 } + : { top: 22, right: 24, bottom: 44, left: 58 }, + }) +} + +export default function FreeCursorExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '88-echarts-free-cursor' + const chartFrameRef = useRef(null) + + const xRef = useRef(null) + + const yRef = useRef(null) + + const sceneRef = useRef>(null) + + const stateRef = useRef(clearedCursor()) + + const lastPositionRef = useRef>({ + x: (freeCursorXDomain[0] + freeCursorXDomain[1]) / 2, + y: (freeCursorYDomain[0] + freeCursorYDomain[1]) / 2, + }) + + const renderCountRef = useRef(0) + + const [accepted, setAccepted] = useState | null>(null) + + const [state, setState] = useState(clearedCursor) + + stateRef.current = state + + const accept = useCallback( + (value: ContinuousCursorPosition | null) => { + const next = value ? roundedPosition(value) : null + if (next) lastPositionRef.current = next + const nextState = next ? cursorState(next, true) : clearedCursor() + stateRef.current = nextState + setAccepted(next) + setState(nextState) + }, + [], + ) + + const handleCursorChange = useCallback( + ( + value: ContinuousCursorPosition | null, + reason: ContinuousCursorChange, + ) => { + if (reason.type === 'preview') { + if (value) lastPositionRef.current = roundedPosition(value) + const nextState = value ? cursorState(value, false) : clearedCursor() + stateRef.current = nextState + setState(nextState) + return + } + accept(value) + }, + [accept], + ) + + const visible = + state.visible && state.xValue !== null && state.yValue !== null + + const xValue = visible ? state.xValue! : lastPositionRef.current.x + + const yValue = visible ? state.yValue! : lastPositionRef.current.y + + const status = visible + ? `${formatFreeCursorValue('HP', xValue)} · ${formatFreeCursorValue( + 'MPG', + yValue, + )}${state.pinned ? ' · pinned' : ''}` + : 'Move the pointer or adjust horsepower and fuel economy' + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape' || !stateRef.current.visible) return + event.preventDefault() + accept(null) + } + + return ( +
+
+ accept({ x, y: yValue })} + /> + accept({ x: xValue, y })} + /> + + {status} + +
+
+ { + sceneRef.current = scene + renderCountRef.current += 1 + }} + /> +
+
+ ) +} diff --git a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.test.ts b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.test.ts index 33ee6c39..d3eff73a 100644 --- a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.test.ts +++ b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.test.ts @@ -224,7 +224,7 @@ describe('definition-owned free cursor', () => { process.cwd(), 'benchmarks/conformance/cases/88-echarts-free-cursor', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') for (const forbidden of [ diff --git a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts index 0173d05f..90c27270 100644 --- a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts +++ b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts @@ -1,154 +1,46 @@ +import { + cursorControlsHeight, + cursorState, + clearedCursor, + roundedPosition, + chartHeight, + freeCursorDefinition, +} from './example' +import type { CursorState } from './example' +export { + cursorControlsHeight, + cursorState, + clearedCursor, + roundedPosition, + chartHeight, + freeCursorDefinition, +} from './example' +export type { CursorState } from './example' import { cars } from '@charts-poc/demo-data/cars' -import { defineChart, dot, lineY } from '@tanstack/charts' -import { continuousCursor } from '@tanstack/charts/interaction/cursor' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { decorative } from '@tanstack/charts/mark/decorative' -import { scaleLinear } from 'd3-scale' import { clientPointBounds, scenePointToClient, } from '../../shared/driver-geometry' import { tanstackCase } from '../../shared/mount' -import { formatFreeCursorValue } from './format' -import { - freeCursorFractionFromAnchor, - freeCursorRows, - freeCursorXDomain, - freeCursorYDomain, -} from './model' +import { freeCursorFractionFromAnchor, freeCursorRows } from './model' import type { ChartScene } from '@tanstack/charts' -import type { - ContinuousCursorChange, - ContinuousCursorPosition, -} from '@tanstack/charts/interaction/cursor' +import type { ContinuousCursorPosition } from '@tanstack/charts/interaction/cursor' import type { CompleteCar } from './model' import type { ConformanceGeometryQuery, ConformanceGeometrySample, - ConformanceInput, ConformanceJsonObject, ConformanceTarget, ConformanceTestDriver, } from '../../types' -export interface CursorState { - visible: boolean - xNormalized: number | null - yNormalized: number | null - xValue: number | null - yValue: number | null - pinned: boolean -} - -export const cursorControlsHeight = 68 +export { default as Example } from './example' const rows = freeCursorRows(cars) const catalogPreviewCursor = { x: 101.8, y: 20.8, } satisfies ContinuousCursorPosition -export function freeCursorDefinition( - position: ContinuousCursorPosition | null, - onChange: ( - value: ContinuousCursorPosition | null, - reason: ContinuousCursorChange, - ) => void, - preview = false, -) { - return defineChart({ - marks: [ - decorative( - lineY(rows, { - id: 'free-cursor-line', - x: 'power (hp)', - y: 'economy (mpg)', - stroke: '#0f766e', - strokeWidth: 2, - }), - ), - dot(rows, { - id: 'free-cursor-dots', - x: 'power (hp)', - y: 'economy (mpg)', - fill: '#0f766e', - r: 3.5, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - x: { - scale: scaleLinear().domain(freeCursorXDomain), - axis: { label: 'Horsepower' }, - }, - y: { - scale: scaleLinear().domain(freeCursorYDomain), - grid: true, - axis: { ticks: { count: 7 }, label: 'Fuel economy (mpg)' }, - }, - controls: [ - continuousCursor({ - position: controlledSignal< - ContinuousCursorPosition | null, - ContinuousCursorChange - >(position, (next, { reason }) => onChange(next, reason)), - xRule: { - stroke: '#64748b', - strokeWidth: 1, - strokeDasharray: '4 4', - }, - yRule: { - stroke: '#64748b', - strokeWidth: 1, - strokeDasharray: '4 4', - }, - marker: { - radius: 4, - fill: '#ffffff', - stroke: '#0f766e', - strokeWidth: 2, - }, - xLabel: { - format: (value) => - formatFreeCursorValue('HP', roundCursorValue(value)), - ...(preview - ? { - offset: 2, - paddingX: 3, - paddingY: 2, - fontSize: 8, - color: 'Canvas', - background: 'CanvasText', - stroke: 'Canvas', - } - : {}), - }, - yLabel: { - side: 'start', - format: (value) => - formatFreeCursorValue('MPG', roundCursorValue(value)), - ...(preview - ? { - offset: 2, - paddingX: 3, - paddingY: 2, - fontSize: 8, - color: 'Canvas', - background: 'CanvasText', - stroke: 'Canvas', - } - : {}), - }, - }), - ], - svgAnimation: false, - keyboard: false, - focusRing: false, - margin: preview - ? { top: 0, right: 0, bottom: 14, left: 40 } - : { top: 22, right: 24, bottom: 44, left: 58 }, - }) -} - export const catalogCase = tanstackCase( (input) => freeCursorDefinition( @@ -271,38 +163,6 @@ function geometry( return sample ? [sample] : [] } -export function cursorState( - position: ContinuousCursorPosition, - pinned: boolean, -): CursorState { - const xValue = roundCursorValue(position.x) - const yValue = roundCursorValue(position.y) - return { - visible: true, - xNormalized: - (xValue - freeCursorXDomain[0]) / - (freeCursorXDomain[1] - freeCursorXDomain[0]), - yNormalized: - 1 - - (yValue - freeCursorYDomain[0]) / - (freeCursorYDomain[1] - freeCursorYDomain[0]), - xValue, - yValue, - pinned, - } -} - -export function clearedCursor(): CursorState { - return { - visible: false, - xNormalized: null, - yNormalized: null, - xValue: null, - yValue: null, - pinned: false, - } -} - function interactionState( state: CursorState, renderCount: number, @@ -321,20 +181,3 @@ function interactionState( render: { count: renderCount }, } } - -export function roundedPosition( - position: ContinuousCursorPosition, -): ContinuousCursorPosition { - return { - x: roundCursorValue(position.x), - y: roundCursorValue(position.y), - } -} - -function roundCursorValue(value: number) { - return Math.round(value * 10) / 10 -} - -export function chartHeight(height: number) { - return Math.max(180, height - cursorControlsHeight) -} diff --git a/benchmarks/conformance/cases/88-echarts-free-cursor/view.tsx b/benchmarks/conformance/cases/88-echarts-free-cursor/view.tsx index 120902e6..87b9b9a2 100644 --- a/benchmarks/conformance/cases/88-echarts-free-cursor/view.tsx +++ b/benchmarks/conformance/cases/88-echarts-free-cursor/view.tsx @@ -12,22 +12,22 @@ import { Chart } from '@tanstack/charts/react' import { reactMount } from '../../shared/react-mount' import { formatFreeCursorValue } from './format' import { freeCursorXDomain, freeCursorYDomain } from './model' +import { createDriver } from './tanstack' import { chartHeight, clearedCursor, - createDriver, cursorControlsHeight, cursorState, freeCursorDefinition, roundedPosition, -} from './tanstack' +} from './example' import type { ChartScene } from '@tanstack/charts' import type { ContinuousCursorChange, ContinuousCursorPosition, } from '@tanstack/charts/interaction/cursor' import type { CompleteCar } from './model' -import type { CursorState } from './tanstack' +import type { CursorState } from './example' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/89-brush-range-selection/example.tsx b/benchmarks/conformance/cases/89-brush-range-selection/example.tsx new file mode 100644 index 00000000..78bfe550 --- /dev/null +++ b/benchmarks/conformance/cases/89-brush-range-selection/example.tsx @@ -0,0 +1,231 @@ +import { useCallback, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import { initialBrushRange, observedBrushDates, monthlyAaplRows } from './model' +import { aapl } from '@charts-poc/demo-data/aapl' +import type { ChartScene } from '@tanstack/charts' +import type { + BrushRange, + BrushXChange, +} from '@tanstack/charts/interaction/brush' +import type { AaplRow } from '@charts-poc/demo-data/aapl' + +export interface BrushState { + range: BrushRange + dragging: boolean +} +import { defineChart, dot, lineY } from '@tanstack/charts' +import { brushX } from '@tanstack/charts/interaction/brush' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { decorative } from '@tanstack/charts/mark/decorative' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { + brushDateKey, + brushDomain, + brushRangeSummary, + brushShortDate, +} from './model' +import { brushSelectionFill } from './paint' +const initialRange = initialBrushRange( + observedBrushDates(monthlyAaplRows(aapl)), +) + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const color = '#2563eb' + +const brushRows = monthlyAaplRows(aapl) + +const brushDates = observedBrushDates(brushRows) + +const fullDomain = brushDomain(brushDates) + +const brushMonthFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + timeZone: 'UTC', +}) + +export function brushRangeDefinition( + range: BrushRange, + onChange: (range: BrushRange, reason: BrushXChange) => void, +) { + return defineChart({ + marks: [ + decorative( + lineY(brushRows, { + id: 'brush-series-line', + x: 'Date', + y: 'Close', + stroke: color, + strokeWidth: 2.5, + }), + ), + dot(brushRows, { + id: 'brush-series-points', + x: 'Date', + y: 'Close', + fill: color, + r: 3.5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleUtc().domain(fullDomain), + axis: { + ticks: { format: (value) => brushMonthFormatter.format(value) }, + label: 'Month', + }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, + }, + controls: [ + brushX({ + id: 'monthly-range', + range: controlledSignal, BrushXChange>( + range, + (next, { reason }) => onChange(next, reason), + ), + values: brushDates, + ariaLabel: + 'Monthly range brush. Drag to select; focus either handle and use arrow keys, Home, or End to adjust.', + startAriaLabel: 'Range start', + endAriaLabel: 'Range end', + format: brushDateKey, + handleSize: 16, + selectionStyle: { + fill: brushSelectionFill, + fillOpacity: 1, + stroke: color, + strokeWidth: 1, + }, + handleStyle: { + fill: 'Canvas', + fillOpacity: 1, + stroke: color, + strokeWidth: 2, + }, + }), + ], + svgAnimation: false, + keyboard: false, + focusRing: false, + margin: { top: 52, right: 24, bottom: 44, left: 58 }, + }) +} + +export function brushRangeStatus(range: BrushRange) { + const summary = brushRangeSummary(brushRows, range) + return { + label: `${brushShortDate(range.start)} → ${brushShortDate(range.end)} · ${summary.count} AAPL closes · avg $${summary.average.toFixed(1)}`, + ariaLabel: `${brushDateKey(range.start)} through ${brushDateKey(range.end)}, ${summary.count} AAPL closing prices, average $${summary.average.toFixed(1)}`, + } +} + +export function copyRange(range: BrushRange): BrushRange { + return { + start: new Date(range.start.getTime()), + end: new Date(range.end.getTime()), + } +} + +export default function BrushRangeExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '89-brush-range-selection' + const shellRef = useRef(null) + + const chartRef = useRef(null) + + const sceneRef = useRef>(null) + + const [accepted, setAccepted] = useState(() => copyRange(initialRange)) + + const [state, setState] = useState(() => ({ + range: copyRange(initialRange), + dragging: false, + })) + + const stateRef = useRef(state) + + stateRef.current = state + + const handleBrushChange = useCallback( + (next: BrushRange, reason: BrushXChange) => { + const nextState = { + range: copyRange(next), + dragging: reason.type === 'preview', + } + stateRef.current = nextState + setState(nextState) + if (reason.type !== 'preview') setAccepted(copyRange(next)) + }, + [], + ) + + const definition = useMemo( + () => brushRangeDefinition(accepted, handleBrushChange), + [accepted, handleBrushChange], + ) + + const status = brushRangeStatus(state.range) + + return ( +
+
+ { + sceneRef.current = scene + }} + /> +
+ + {status.label} + +
+ ) +} diff --git a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.test.ts b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.test.ts index a9be5834..270e18fe 100644 --- a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.test.ts +++ b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.test.ts @@ -131,7 +131,7 @@ describe('definition-owned brush range', () => { process.cwd(), 'benchmarks/conformance/cases/89-brush-range-selection', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') for (const forbidden of [ diff --git a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts index b628dfd2..f2ebb883 100644 --- a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts +++ b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts @@ -1,9 +1,8 @@ +import { brushRangeDefinition, brushRangeStatus, copyRange } from './example' +import type { BrushState } from './example' +export { brushRangeDefinition, brushRangeStatus, copyRange } from './example' +export type { BrushState } from './example' import { aapl } from '@charts-poc/demo-data/aapl' -import { defineChart, dot, lineY } from '@tanstack/charts' -import { brushX } from '@tanstack/charts/interaction/brush' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { decorative } from '@tanstack/charts/mark/decorative' -import { scaleLinear, scaleUtc } from 'd3-scale' import { clientPointBounds, scenePointToClient, @@ -12,19 +11,14 @@ import { tanstackCase } from '../../shared/mount' import { brushDateFromAnchor, brushDateKey, - brushDomain, brushRangeSummary, - brushShortDate, initialBrushRange, monthlyAaplRows, observedBrushDates, } from './model' -import { brushSelectionFill, normalizedElementFill } from './paint' +import { normalizedElementFill } from './paint' import type { AaplRow } from '@charts-poc/demo-data/aapl' -import type { - BrushRange, - BrushXChange, -} from '@tanstack/charts/interaction/brush' +import type { BrushRange } from '@tanstack/charts/interaction/brush' import type { ChartScene } from '@tanstack/charts' import type { ConformanceGeometryQuery, @@ -34,91 +28,11 @@ import type { ConformanceTestDriver, } from '../../types' -export interface BrushState { - range: BrushRange - dragging: boolean -} +export { default as Example } from './example' const color = '#2563eb' const brushRows = monthlyAaplRows(aapl) const brushDates = observedBrushDates(brushRows) -const fullDomain = brushDomain(brushDates) -const brushMonthFormatter = new Intl.DateTimeFormat('en-US', { - month: 'short', - timeZone: 'UTC', -}) - -export function brushRangeDefinition( - range: BrushRange, - onChange: (range: BrushRange, reason: BrushXChange) => void, -) { - return defineChart({ - marks: [ - decorative( - lineY(brushRows, { - id: 'brush-series-line', - x: 'Date', - y: 'Close', - stroke: color, - strokeWidth: 2.5, - }), - ), - dot(brushRows, { - id: 'brush-series-points', - x: 'Date', - y: 'Close', - fill: color, - r: 3.5, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - x: { - scale: scaleUtc().domain(fullDomain), - axis: { - ticks: { format: (value) => brushMonthFormatter.format(value) }, - label: 'Month', - }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, - }, - controls: [ - brushX({ - id: 'monthly-range', - range: controlledSignal, BrushXChange>( - range, - (next, { reason }) => onChange(next, reason), - ), - values: brushDates, - ariaLabel: - 'Monthly range brush. Drag to select; focus either handle and use arrow keys, Home, or End to adjust.', - startAriaLabel: 'Range start', - endAriaLabel: 'Range end', - format: brushDateKey, - handleSize: 16, - selectionStyle: { - fill: brushSelectionFill, - fillOpacity: 1, - stroke: color, - strokeWidth: 1, - }, - handleStyle: { - fill: 'Canvas', - fillOpacity: 1, - stroke: color, - strokeWidth: 2, - }, - }), - ], - svgAnimation: false, - keyboard: false, - focusRing: false, - margin: { top: 52, right: 24, bottom: 44, left: 58 }, - }) -} export const catalogCase = tanstackCase( () => brushRangeDefinition(initialBrushRange(brushDates), () => {}), @@ -260,21 +174,6 @@ function brushGeometry( ] } -export function brushRangeStatus(range: BrushRange) { - const summary = brushRangeSummary(brushRows, range) - return { - label: `${brushShortDate(range.start)} → ${brushShortDate(range.end)} · ${summary.count} AAPL closes · avg $${summary.average.toFixed(1)}`, - ariaLabel: `${brushDateKey(range.start)} through ${brushDateKey(range.end)}, ${summary.count} AAPL closing prices, average $${summary.average.toFixed(1)}`, - } -} - -export function copyRange(range: BrushRange): BrushRange { - return { - start: new Date(range.start.getTime()), - end: new Date(range.end.getTime()), - } -} - function center(element: HTMLElement | SVGElement) { const bounds = element.getBoundingClientRect() return { diff --git a/benchmarks/conformance/cases/89-brush-range-selection/view.tsx b/benchmarks/conformance/cases/89-brush-range-selection/view.tsx index 7fd98814..b379313d 100644 --- a/benchmarks/conformance/cases/89-brush-range-selection/view.tsx +++ b/benchmarks/conformance/cases/89-brush-range-selection/view.tsx @@ -10,19 +10,15 @@ import { Chart } from '@tanstack/charts/react' import { reactMount } from '../../shared/react-mount' import { initialBrushRange, observedBrushDates, monthlyAaplRows } from './model' import { aapl } from '@charts-poc/demo-data/aapl' -import { - brushRangeDefinition, - brushRangeStatus, - copyRange, - createDriver, -} from './tanstack' +import { createDriver } from './tanstack' +import { brushRangeDefinition, brushRangeStatus, copyRange } from './example' import type { ChartScene } from '@tanstack/charts' import type { BrushRange, BrushXChange, } from '@tanstack/charts/interaction/brush' import type { AaplRow } from '@charts-poc/demo-data/aapl' -import type { BrushState } from './tanstack' +import type { BrushState } from './example' import type { ConformanceTestDriver } from '../../types' import type { ReactConformanceProps } from '../../shared/react-mount' diff --git a/benchmarks/conformance/cases/90-zoomable-time-window/example.tsx b/benchmarks/conformance/cases/90-zoomable-time-window/example.tsx new file mode 100644 index 00000000..ef7c4da6 --- /dev/null +++ b/benchmarks/conformance/cases/90-zoomable-time-window/example.tsx @@ -0,0 +1,268 @@ +import { useCallback, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import { initialZoomWindow } from './model' +import type { AaplRow } from '@charts-poc/demo-data/aapl' +import type { ChartScene } from '@tanstack/charts' +import type { + ZoomXChange, + ZoomXWindow, +} from '@tanstack/charts/interaction/zoom' +import { aapl } from '@charts-poc/demo-data/aapl' +import { defineChart, dot, lineY } from '@tanstack/charts' +import { zoomX } from '@tanstack/charts/interaction/zoom' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { decorative } from '@tanstack/charts/mark/decorative' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { + selectZoomRows, + visibleZoomData, + zoomDateKey, + zoomFullDomain, + zoomSpanDays, +} from './model' +import type { ZoomXAction } from '@tanstack/charts/interaction/zoom' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export interface ZoomState { + window: ZoomXWindow + lastAction: 'none' | ZoomXAction + active: boolean + wheelCaptured: boolean +} + +const color = '#0f766e' + +const zoomRows = selectZoomRows(aapl) + +const zoomDateFormatter = new Intl.DateTimeFormat('en-US', { + month: 'short', + day: 'numeric', + timeZone: 'UTC', +}) + +export function zoomTimeWindowDefinition( + window: ZoomXWindow, + onChange: (window: ZoomXWindow, reason: ZoomXChange) => void, + onActiveChange?: (active: boolean) => void, +) { + const rows = visibleZoomData(zoomRows, window) + return defineChart({ + marks: [ + decorative( + lineY(rows, { + id: 'zoom-series-line', + x: 'Date', + y: 'Close', + stroke: color, + strokeWidth: 2.5, + }), + ), + dot(rows, { + id: 'zoom-series-points', + x: 'Date', + y: 'Close', + fill: color, + r: 3.5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleUtc().domain([window.start, window.end]), + axis: { + ticks: { format: (value) => zoomDateFormatter.format(value) }, + label: 'Date', + }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, + }, + controls: [ + zoomX({ + id: 'time-window', + window: controlledSignal, ZoomXChange>( + window, + (next, { reason }) => onChange(next, reason), + ), + extent: zoomFullDomain, + scaleExtent: [1, 8], + ariaLabel: + 'Zoomable time window. Focus the chart before wheel zoom; drag or use a horizontal wheel to pan; use plus, minus, arrow keys, or Home.', + ariaDescription: + 'Wheel zoom; drag or horizontal wheel pan; plus and minus zoom; arrows pan; Home resets.', + format: zoomDateKey, + onActiveChange, + }), + ], + svgAnimation: false, + keyboard: false, + focusRing: false, + margin: { top: 56, right: 24, bottom: 44, left: 58 }, + }) +} + +export function zoomStatusLabel(state: ZoomState) { + return state.active + ? `${zoomDateKey(state.window.start)} → ${zoomDateKey(state.window.end)} · ${formatSpan(zoomSpanDays(state.window))} days` + : 'Focus chart to zoom' +} + +export function copyWindow(window: ZoomXWindow): ZoomXWindow { + return { + start: new Date(window.start.getTime()), + end: new Date(window.end.getTime()), + } +} + +function formatSpan(days: number) { + return Number.isInteger(days) ? String(days) : days.toFixed(1) +} + +export default function ZoomTimeWindowExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '90-zoomable-time-window' + const shellRef = useRef(null) + + const chartRef = useRef(null) + + const sceneRef = useRef>(null) + + const [accepted, setAccepted] = useState(() => copyWindow(initialZoomWindow)) + + const [state, setState] = useState(() => ({ + window: copyWindow(initialZoomWindow), + lastAction: 'none', + active: false, + wheelCaptured: false, + })) + + const stateRef = useRef(state) + + stateRef.current = state + + const handleZoomChange = useCallback( + (next: ZoomXWindow, reason: ZoomXChange) => { + const nextWindow = copyWindow(next) + const nextState: ZoomState = { + ...stateRef.current, + window: nextWindow, + lastAction: reason.action, + wheelCaptured: + stateRef.current.wheelCaptured || reason.source === 'wheel', + } + stateRef.current = nextState + setAccepted(nextWindow) + setState(nextState) + }, + [], + ) + + const handleActiveChange = useCallback((active: boolean) => { + const nextState = { ...stateRef.current, active } + stateRef.current = nextState + setState(nextState) + }, []) + + const definition = useMemo( + () => + zoomTimeWindowDefinition(accepted, handleZoomChange, handleActiveChange), + [accepted, handleActiveChange, handleZoomChange], + ) + + const reset = () => { + const nextWindow = copyWindow(initialZoomWindow) + const nextState: ZoomState = { + ...stateRef.current, + window: nextWindow, + lastAction: 'reset', + } + stateRef.current = nextState + setAccepted(nextWindow) + setState(nextState) + chartRef.current + ?.querySelector('[data-chart-zoom-surface]') + ?.focus() + } + + return ( +
+
+ { + sceneRef.current = scene + }} + /> +
+ + {zoomStatusLabel(state)} + + +
+ ) +} diff --git a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.test.ts b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.test.ts index fed62894..f3f7fdaa 100644 --- a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.test.ts +++ b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.test.ts @@ -135,7 +135,7 @@ describe('definition-owned zoomable time window', () => { process.cwd(), 'benchmarks/conformance/cases/90-zoomable-time-window', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') for (const forbidden of [ diff --git a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts index 29dd3104..09915788 100644 --- a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts +++ b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts @@ -1,9 +1,16 @@ +import { + zoomTimeWindowDefinition, + zoomStatusLabel, + copyWindow, +} from './example' +import type { ZoomState } from './example' +export { + zoomTimeWindowDefinition, + zoomStatusLabel, + copyWindow, +} from './example' +export type { ZoomState } from './example' import { aapl } from '@charts-poc/demo-data/aapl' -import { defineChart, dot, lineY } from '@tanstack/charts' -import { zoomX } from '@tanstack/charts/interaction/zoom' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { decorative } from '@tanstack/charts/mark/decorative' -import { scaleLinear, scaleUtc } from 'd3-scale' import { clientPointBounds, scenePointToClient, @@ -15,15 +22,10 @@ import { visibleZoomData, zoomDateFromAnchor, zoomDateKey, - zoomFullDomain, zoomSpanDays, } from './model' import type { AaplRow } from '@charts-poc/demo-data/aapl' -import type { - ZoomXAction, - ZoomXChange, - ZoomXWindow, -} from '@tanstack/charts/interaction/zoom' +import type { ZoomXWindow } from '@tanstack/charts/interaction/zoom' import type { ChartScene } from '@tanstack/charts' import type { ConformanceGeometryQuery, @@ -33,88 +35,15 @@ import type { ConformanceTestDriver, } from '../../types' -export interface ZoomState { - window: ZoomXWindow - lastAction: 'none' | ZoomXAction - active: boolean - wheelCaptured: boolean -} +export { default as Example } from './example' const color = '#0f766e' const zoomRows = selectZoomRows(aapl) -const zoomDateFormatter = new Intl.DateTimeFormat('en-US', { - month: 'short', - day: 'numeric', - timeZone: 'UTC', -}) const catalogPreviewWindow: ZoomXWindow = { start: new Date(Date.UTC(2018, 0, 8)), end: new Date(Date.UTC(2018, 0, 16)), } -export function zoomTimeWindowDefinition( - window: ZoomXWindow, - onChange: (window: ZoomXWindow, reason: ZoomXChange) => void, - onActiveChange?: (active: boolean) => void, -) { - const rows = visibleZoomData(zoomRows, window) - return defineChart({ - marks: [ - decorative( - lineY(rows, { - id: 'zoom-series-line', - x: 'Date', - y: 'Close', - stroke: color, - strokeWidth: 2.5, - }), - ), - dot(rows, { - id: 'zoom-series-points', - x: 'Date', - y: 'Close', - fill: color, - r: 3.5, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - x: { - scale: scaleUtc().domain([window.start, window.end]), - axis: { - ticks: { format: (value) => zoomDateFormatter.format(value) }, - label: 'Date', - }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, - }, - controls: [ - zoomX({ - id: 'time-window', - window: controlledSignal, ZoomXChange>( - window, - (next, { reason }) => onChange(next, reason), - ), - extent: zoomFullDomain, - scaleExtent: [1, 8], - ariaLabel: - 'Zoomable time window. Focus the chart before wheel zoom; drag or use a horizontal wheel to pan; use plus, minus, arrow keys, or Home.', - ariaDescription: - 'Wheel zoom; drag or horizontal wheel pan; plus and minus zoom; arrows pan; Home resets.', - format: zoomDateKey, - onActiveChange, - }), - ], - svgAnimation: false, - keyboard: false, - focusRing: false, - margin: { top: 56, right: 24, bottom: 44, left: 58 }, - }) -} - export const catalogCase = tanstackCase( (input) => zoomTimeWindowDefinition( @@ -245,23 +174,6 @@ function zoomGeometry( return sample ? [sample] : [] } -export function zoomStatusLabel(state: ZoomState) { - return state.active - ? `${zoomDateKey(state.window.start)} → ${zoomDateKey(state.window.end)} · ${formatSpan(zoomSpanDays(state.window))} days` - : 'Focus chart to zoom' -} - -export function copyWindow(window: ZoomXWindow): ZoomXWindow { - return { - start: new Date(window.start.getTime()), - end: new Date(window.end.getTime()), - } -} - -function formatSpan(days: number) { - return Number.isInteger(days) ? String(days) : days.toFixed(1) -} - function center(element: HTMLElement | SVGElement) { const bounds = element.getBoundingClientRect() return { diff --git a/benchmarks/conformance/cases/90-zoomable-time-window/view.tsx b/benchmarks/conformance/cases/90-zoomable-time-window/view.tsx index 5b4ad8a4..34d11274 100644 --- a/benchmarks/conformance/cases/90-zoomable-time-window/view.tsx +++ b/benchmarks/conformance/cases/90-zoomable-time-window/view.tsx @@ -9,12 +9,12 @@ import { import { Chart } from '@tanstack/charts/react' import { reactMount } from '../../shared/react-mount' import { initialZoomWindow } from './model' +import { createDriver } from './tanstack' import { copyWindow, - createDriver, zoomStatusLabel, zoomTimeWindowDefinition, -} from './tanstack' +} from './example' import type { AaplRow } from '@charts-poc/demo-data/aapl' import type { ChartScene } from '@tanstack/charts' import type { @@ -23,7 +23,7 @@ import type { } from '@tanstack/charts/interaction/zoom' import type { ReactConformanceProps } from '../../shared/react-mount' import type { ConformanceTestDriver } from '../../types' -import type { ZoomState } from './tanstack' +import type { ZoomState } from './example' const ZoomTimeWindowExample = forwardRef< ConformanceTestDriver, diff --git a/benchmarks/conformance/cases/91-timeline-playback-scrubber/example.tsx b/benchmarks/conformance/cases/91-timeline-playback-scrubber/example.tsx new file mode 100644 index 00000000..e0ff5815 --- /dev/null +++ b/benchmarks/conformance/cases/91-timeline-playback-scrubber/example.tsx @@ -0,0 +1,374 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import type { AaplRow } from '@charts-poc/demo-data/aapl' +import type { ChartScene } from '@tanstack/charts' +import type { HandleXChange } from '@tanstack/charts/interaction/handle' + +export interface PlaybackState { + frame: Date + dragging: boolean + scrubCount: number + playing: boolean +} +import { defineChart, dot, lineY } from '@tanstack/charts' +import { aapl } from '@charts-poc/demo-data/aapl' +import { handleX } from '@tanstack/charts/interaction/handle' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { decorative } from '@tanstack/charts/mark/decorative' +import { scaleLinear, scaleUtc } from 'd3-scale' +import { + initialPlaybackIndex, + playbackDateKey, + selectPlaybackRows, +} from './model' +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +const linePaint = '#2563eb' + +const playheadPaint = '#f97316' + +const margin = { top: 64, right: 24, bottom: 68, left: 56 } + +export const playbackRows = selectPlaybackRows(aapl) + +const playbackDates = playbackRows.map((row) => row.Date) + +export const initialFrame = playbackRows[initialPlaybackIndex]?.Date + +export function playbackDefinition( + frame: Date, + onChange: (value: Date, reason: HandleXChange) => void, + preview = false, +) { + return defineChart({ + marks: [ + decorative( + lineY(playbackRows, { + id: 'playback-line', + x: 'Date', + y: 'Close', + stroke: linePaint, + strokeWidth: 2.5, + }), + ), + dot(playbackRows, { + id: 'playback-points', + x: 'Date', + y: 'Close', + fill: linePaint, + r: 3.5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + x: { + scale: scaleUtc, + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, + }, + controls: [ + handleX({ + id: 'playback-frame', + value: controlledSignal>( + frame, + (next, { reason }) => onChange(next, reason), + ), + values: playbackDates, + cross: { edge: 'bottom', offset: preview ? -18 : 34 }, + trackStyle: { + fill: 'color-mix(in srgb, currentColor 52%, transparent)', + }, + ruleStyle: { fill: playheadPaint }, + handleStyle: { + fill: playheadPaint, + stroke: 'Canvas', + strokeWidth: 2, + }, + hitSize: 44, + ariaLabel: 'Timeline frame', + format: (value) => playbackValueText(rowForDate(value)), + }), + ], + svgAnimation: false, + keyboard: false, + focusRing: false, + margin: preview ? 0 : margin, + }) +} + +export function rowForDate(date: Date) { + const row = playbackRows.find( + (candidate) => candidate.Date.getTime() === date.getTime(), + ) + if (!row) throw new Error('Playback frame must be an observed date.') + return row +} + +export function indexForDate(date: Date) { + const index = playbackRows.findIndex( + (row) => row.Date.getTime() === date.getTime(), + ) + if (index < 0) throw new Error('Playback frame must be an observed date.') + return index +} + +export function playbackValueText(row: AaplRow) { + return `${playbackDateKey(row.Date)} · AAPL close $${row.Close.toFixed(2)}` +} + +export function cloneDate(date: Date) { + return new Date(date.getTime()) +} + +export default function PlaybackExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '91-timeline-playback-scrubber' + const viewRef = useRef(null) + + const chartRef = useRef(null) + + const playRef = useRef(null) + + const sceneRef = useRef>(null) + + const timerRef = useRef | undefined>(undefined) + + const [accepted, setAccepted] = useState(() => cloneDate(initialFrame)) + + const [state, setState] = useState(() => ({ + frame: cloneDate(initialFrame), + dragging: false, + scrubCount: 0, + playing: false, + })) + + const [announcement, setAnnouncement] = useState('') + + const stateRef = useRef(state) + + stateRef.current = state + + const commitState = useCallback((next: PlaybackState) => { + stateRef.current = next + setState(next) + }, []) + + const frameText = useCallback( + (frame = stateRef.current.frame) => playbackValueText(rowForDate(frame)), + [], + ) + + const stopPlayback = useCallback( + (message?: string) => { + if (timerRef.current !== undefined) clearInterval(timerRef.current) + timerRef.current = undefined + commitState({ ...stateRef.current, playing: false }) + if (message) setAnnouncement(`${message}. ${frameText()}`) + }, + [commitState, frameText], + ) + + const applyFrame = useCallback( + (next: Date) => { + const frame = cloneDate(next) + setAccepted(frame) + commitState({ ...stateRef.current, frame }) + }, + [commitState], + ) + + const handleFrameChange = useCallback( + (next: Date, reason: HandleXChange) => { + if (stateRef.current.playing) stopPlayback() + if (reason.type === 'preview') { + commitState({ + ...stateRef.current, + frame: cloneDate(next), + dragging: true, + }) + return + } + if (reason.type === 'cancel') { + const frame = cloneDate(reason.origin) + commitState({ ...stateRef.current, frame, dragging: false }) + setAnnouncement(`Scrub canceled. ${frameText(frame)}`) + return + } + const frame = cloneDate(next) + setAccepted(frame) + commitState({ + ...stateRef.current, + frame, + dragging: false, + scrubCount: stateRef.current.scrubCount + 1, + }) + setAnnouncement(`Frame selected. ${frameText(frame)}`) + }, + [commitState, frameText, stopPlayback], + ) + + const definition = useMemo( + () => playbackDefinition(accepted, handleFrameChange), + [accepted, handleFrameChange], + ) + + const togglePlayback = useCallback(() => { + if (stateRef.current.playing) { + stopPlayback('Playback paused') + return + } + const lastIndex = playbackRows.length - 1 + const restarting = indexForDate(stateRef.current.frame) >= lastIndex + if (restarting) applyFrame(playbackRows[0]!.Date) + commitState({ ...stateRef.current, playing: true, dragging: false }) + timerRef.current = setInterval(() => { + const index = indexForDate(stateRef.current.frame) + if (index >= playbackRows.length - 1) { + stopPlayback('Playback ended') + return + } + applyFrame(playbackRows[index + 1]!.Date) + }, 700) + setAnnouncement( + `${restarting ? 'Playback restarted' : 'Playback started'}. ${frameText()}`, + ) + }, [applyFrame, commitState, frameText, stopPlayback]) + + useEffect( + () => () => { + if (timerRef.current !== undefined) clearInterval(timerRef.current) + }, + [], + ) + + const buttonLabel = state.playing ? 'Pause timeline' : 'Play timeline' + + return ( +
+
+ { + sceneRef.current = scene + }} + /> +
+
+
+ {frameText(state.frame)} +
+ +
+ + {announcement} + +
+ ) +} diff --git a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.test.ts b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.test.ts index 0a2df224..4ede1267 100644 --- a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.test.ts +++ b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.test.ts @@ -124,7 +124,7 @@ describe('definition-owned playback handle', () => { process.cwd(), 'benchmarks/conformance/cases/91-timeline-playback-scrubber', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') expect(existsSync(resolve(directory, 'overlay.ts'))).toBe(true) diff --git a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts index f4c860fe..a1b517ce 100644 --- a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts +++ b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts @@ -1,22 +1,30 @@ -import { defineChart, dot, lineY } from '@tanstack/charts' -import { aapl } from '@charts-poc/demo-data/aapl' -import { handleX } from '@tanstack/charts/interaction/handle' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { decorative } from '@tanstack/charts/mark/decorative' -import { scaleLinear, scaleUtc } from 'd3-scale' +import { + playbackRows, + initialFrame, + playbackDefinition, + rowForDate, + indexForDate, + playbackValueText, + cloneDate, +} from './example' +import type { PlaybackState } from './example' +export { + playbackRows, + initialFrame, + playbackDefinition, + rowForDate, + indexForDate, + playbackValueText, + cloneDate, +} from './example' +export type { PlaybackState } from './example' import { clientPointBounds, scenePointToClient, } from '../../shared/driver-geometry' -import { - initialPlaybackIndex, - playbackDateKey, - playbackIndexFromAnchor, - selectPlaybackRows, -} from './model' +import { playbackDateKey, playbackIndexFromAnchor } from './model' import { tanstackCase } from '../../shared/mount' import type { AaplRow } from '@charts-poc/demo-data/aapl' -import type { HandleXChange } from '@tanstack/charts/interaction/handle' import type { ChartScene } from '@tanstack/charts' import type { ConformanceGeometryQuery, @@ -26,95 +34,11 @@ import type { ConformanceTestDriver, } from '../../types' -export interface PlaybackState { - frame: Date - dragging: boolean - scrubCount: number - playing: boolean -} +export { default as Example } from './example' const linePaint = '#2563eb' -const playheadPaint = '#f97316' -const margin = { top: 64, right: 24, bottom: 68, left: 56 } -export const playbackRows = selectPlaybackRows(aapl) -const playbackDates = playbackRows.map((row) => row.Date) -export const initialFrame = playbackRows[initialPlaybackIndex]?.Date if (!initialFrame) throw new Error('Playback requires an initial frame.') -export function playbackDefinition( - frame: Date, - onChange: (value: Date, reason: HandleXChange) => void, - preview = false, -) { - return defineChart({ - marks: [ - decorative( - lineY(playbackRows, { - id: 'playback-line', - x: 'Date', - y: 'Close', - stroke: linePaint, - strokeWidth: 2.5, - }), - ), - dot(playbackRows, { - id: 'playback-points', - x: 'Date', - y: 'Close', - fill: linePaint, - r: 3.5, - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - x: { - scale: scaleUtc, - axis: { - ticks: { - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), - }, - }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, - }, - controls: [ - handleX({ - id: 'playback-frame', - value: controlledSignal>( - frame, - (next, { reason }) => onChange(next, reason), - ), - values: playbackDates, - cross: { edge: 'bottom', offset: preview ? -18 : 34 }, - trackStyle: { - fill: 'color-mix(in srgb, currentColor 52%, transparent)', - }, - ruleStyle: { fill: playheadPaint }, - handleStyle: { - fill: playheadPaint, - stroke: 'Canvas', - strokeWidth: 2, - }, - hitSize: 44, - ariaLabel: 'Timeline frame', - format: (value) => playbackValueText(rowForDate(value)), - }), - ], - svgAnimation: false, - keyboard: false, - focusRing: false, - margin: preview ? 0 : margin, - }) -} - export const catalogCase = tanstackCase( () => playbackDefinition(initialFrame, () => {}, true), 'AAPL closes with a draggable timeline playback scrubber', @@ -254,30 +178,6 @@ function elementGeometry(element: SVGElement): ConformanceGeometrySample { } } -export function rowForDate(date: Date) { - const row = playbackRows.find( - (candidate) => candidate.Date.getTime() === date.getTime(), - ) - if (!row) throw new Error('Playback frame must be an observed date.') - return row -} - -export function indexForDate(date: Date) { - const index = playbackRows.findIndex( - (row) => row.Date.getTime() === date.getTime(), - ) - if (index < 0) throw new Error('Playback frame must be an observed date.') - return index -} - -export function playbackValueText(row: AaplRow) { - return `${playbackDateKey(row.Date)} · AAPL close $${row.Close.toFixed(2)}` -} - -export function cloneDate(date: Date) { - return new Date(date.getTime()) -} - function center(element: HTMLElement | SVGElement) { const bounds = element.getBoundingClientRect() return { diff --git a/benchmarks/conformance/cases/91-timeline-playback-scrubber/view.tsx b/benchmarks/conformance/cases/91-timeline-playback-scrubber/view.tsx index 0b24fbea..b6183a5d 100644 --- a/benchmarks/conformance/cases/91-timeline-playback-scrubber/view.tsx +++ b/benchmarks/conformance/cases/91-timeline-playback-scrubber/view.tsx @@ -9,22 +9,22 @@ import { } from 'react' import { Chart } from '@tanstack/charts/react' import { reactMount } from '../../shared/react-mount' +import { createDriver } from './tanstack' import { cloneDate, - createDriver, indexForDate, initialFrame, playbackDefinition, playbackRows, playbackValueText, rowForDate, -} from './tanstack' +} from './example' import type { AaplRow } from '@charts-poc/demo-data/aapl' import type { ChartScene } from '@tanstack/charts' import type { HandleXChange } from '@tanstack/charts/interaction/handle' import type { ReactConformanceProps } from '../../shared/react-mount' import type { ConformanceTestDriver } from '../../types' -import type { PlaybackState } from './tanstack' +import type { PlaybackState } from './example' const PlaybackExample = forwardRef< ConformanceTestDriver, diff --git a/benchmarks/conformance/cases/92-editable-event-range/example.tsx b/benchmarks/conformance/cases/92-editable-event-range/example.tsx new file mode 100644 index 00000000..3267fbfb --- /dev/null +++ b/benchmarks/conformance/cases/92-editable-event-range/example.tsx @@ -0,0 +1,531 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Chart } from '@tanstack/charts/react' +import { + clampEditableEventEnd, + editableDateFromAnchor, + editableDateKey, + editableEventEndValues, +} from './model' +import { editableEvents, initialEditableEventEnd } from './scenario' +import type { ChartScene } from '@tanstack/charts' +import type { HandleXChange } from '@tanstack/charts/interaction/handle' +import type { FormEvent, KeyboardEvent, PointerEvent } from 'react' +import type { EditableEvent } from './scenario' + +export interface EditableState { + end: Date + editing: boolean + editCount: number + originEnd: Date | null +} +import { defineChart, rect, text } from '@tanstack/charts' +import { handleX } from '@tanstack/charts/interaction/handle' +import { controlledSignal } from '@tanstack/charts/interaction/signal' +import { scaleBand, scaleUtc } from 'd3-scale' +import { editableEventColor } from './colors' +import { editableDurationDays } from './model' +import { editableDomain, editableEventStart, editableLanes } from './scenario' +const validationMessage = 'Choose a release end date within the range.' + +export interface ExampleProps { + width?: number + height?: number + revision?: number +} + +export interface EditableChartInput extends ExampleChartInput { + end: Date +} + +const margin = { top: 96, right: 26, bottom: 48, left: 82 } + +const handleId = 'release-end' + +export function editableEventDefinition( + input: EditableChartInput, + onEndChange: (value: Date, reason: HandleXChange) => void, +) { + const rows = editableEvents(input.revision, input.end) + const outsideLabels = rows + .filter((row) => row.id !== 'release') + .map((row) => ({ ...row, labelDate: row.end })) + + return defineChart(({ width }) => { + const releaseLabels = rows + .filter( + (row) => + row.id === 'release' && eventBarCanFitLabel(row, width, 'Release'), + ) + .map((row) => ({ + ...row, + labelDate: row.start, + shortLabel: 'Release', + })) + return { + marks: [ + rect(rows, { + id: 'event-ranges', + x1: 'start', + x2: 'end', + y: 'lane', + color: 'id', + radius: 5, + stroke: '#ffffff', + strokeWidth: 1, + }), + ...(input.preview === true + ? [] + : [ + text(outsideLabels, { + id: 'event-labels', + x: 'labelDate', + y: 'lane', + text: 'label', + anchor: 'start', + dx: 5, + fill: 'currentColor', + fontSize: 10, + fontWeight: 600, + }), + text(releaseLabels, { + id: 'release-label', + x: 'labelDate', + y: 'lane', + text: 'shortLabel', + anchor: 'start', + dx: 5, + fill: '#431407', + fontSize: 10, + fontWeight: 700, + }), + ]), + ], + x: { + scale: scaleUtc().domain(editableDomain), + grid: true, + axis: { + ticks: { + format: (value: Date) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + }, + }, + y: { + scale: scaleBand() + .domain(editableLanes) + .paddingInner(0.38) + .paddingOuter(0.19), + grid: false, + }, + color: { + domain: ['discovery', 'design', 'campaign', 'release'], + range: [ + editableEventColor('discovery'), + editableEventColor('design'), + editableEventColor('campaign'), + editableEventColor('release'), + ], + }, + controls: [ + handleX({ + id: handleId, + value: controlledSignal>( + input.end, + (next, { reason }) => onEndChange(next, reason), + ), + values: editableEventEndValues, + cross: { value: 'Engineering' }, + trackStyle: { + fill: 'color-mix(in srgb, var(--ts-chart-2, #f97316) 58%, transparent)', + }, + ruleStyle: false, + handleStyle: { + fill: 'var(--ts-chart-2, #f97316)', + stroke: 'Canvas', + strokeWidth: 2, + }, + hitSize: 44, + ariaLabel: 'Release end handle', + format: (value) => editableHandleValueText(value), + }), + ], + svgAnimation: false, + keyboard: false, + focusRing: false, + margin, + } + }) +} + +function editableHandleValueText(end: Date) { + return `Release: ${editableDateKey(editableEventStart)} → ${editableDateKey(end)} · ${editableDurationDays(editableEventStart, end)} days` +} + +export function editableSummaryText(end: Date) { + return `Release · ${compactDate(editableEventStart)} → ${compactDate(end)} · ${editableDurationDays(editableEventStart, end)} days` +} + +function compactDate(date: Date) { + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }) +} + +export function editableAriaLabel(revision: number, end: Date) { + return `Editable schedule. ${editableEvents(revision, end) + .map( + (row) => + `${row.label}, ${editableDateKey(row.start)} to ${editableDateKey(row.end)}`, + ) + .join('. ')}.` +} + +function eventBarCanFitLabel( + event: EditableEvent, + width: number, + label: string, +) { + const plotWidth = Math.max(0, width - margin.left - margin.right) + const domainWidth = editableDomain[1].getTime() - editableDomain[0].getTime() + const eventWidth = event.end.getTime() - event.start.getTime() + const barWidth = domainWidth > 0 ? (eventWidth / domainWidth) * plotWidth : 0 + return barWidth >= label.length * 6 + 10 +} + +export function cloneDate(date: Date) { + return new Date(date.getTime()) +} + +export interface ExampleChartInput { + width: number + height: number + revision: number + preview?: boolean + interactive?: boolean +} + +export default function EditableEventExample({ + width = 640, + height = 480, + revision = 0, +}: ExampleProps = {}) { + const input = { width, height, revision, preview: false, interactive: true } + const idPrefix = '92-editable-event-range' + const viewRef = useRef(null) + + const chartRef = useRef(null) + + const dateRef = useRef(null) + + const sceneRef = useRef | null>(null) + + const inputRef = useRef(input) + + inputRef.current = input + + const [acceptedEnd, setAcceptedEnd] = useState(() => + cloneDate(initialEditableEventEnd), + ) + + const [state, setState] = useState(() => ({ + end: cloneDate(initialEditableEventEnd), + editing: false, + editCount: 0, + originEnd: null, + })) + + const [dateValue, setDateValue] = useState(() => + editableDateKey(initialEditableEventEnd), + ) + + const [invalid, setInvalid] = useState(false) + + const stateRef = useRef(state) + + stateRef.current = state + + const commitState = useCallback((next: EditableState) => { + stateRef.current = next + setState(next) + }, []) + + const beginEdit = useCallback( + (origin = stateRef.current.end) => { + if (stateRef.current.editing) return + commitState({ + ...stateRef.current, + originEnd: cloneDate(origin), + editing: true, + }) + }, + [commitState], + ) + + const applyEnd = useCallback( + (next: Date) => { + const end = clampEditableEventEnd(next) + setAcceptedEnd(end) + setDateValue(editableDateKey(end)) + setInvalid(false) + commitState({ ...stateRef.current, end: cloneDate(end) }) + }, + [commitState], + ) + + const commitEdit = useCallback(() => { + if (!stateRef.current.editing) return + commitState({ + ...stateRef.current, + editing: false, + originEnd: null, + editCount: stateRef.current.editCount + 1, + }) + }, [commitState]) + + const cancelEdit = useCallback( + (fallback?: Date) => { + if (!stateRef.current.editing && !fallback) return + const origin = fallback ?? stateRef.current.originEnd + const end = origin ? clampEditableEventEnd(origin) : stateRef.current.end + setAcceptedEnd(end) + setDateValue(editableDateKey(end)) + setInvalid(false) + commitState({ + ...stateRef.current, + end: cloneDate(end), + editing: false, + originEnd: null, + }) + }, + [commitState], + ) + + const handleEndChange = useCallback( + (next: Date, reason: HandleXChange) => { + if (reason.type === 'preview') { + beginEdit(reason.origin) + applyEnd(next) + return + } + if (reason.type === 'cancel') { + cancelEdit(reason.origin) + return + } + beginEdit(reason.origin) + applyEnd(next) + commitEdit() + }, + [applyEnd, beginEdit, cancelEdit, commitEdit], + ) + + const definition = useMemo( + () => + editableEventDefinition({ ...input, end: acceptedEnd }, handleEndChange), + [acceptedEnd, handleEndChange, input], + ) + + useEffect(() => { + dateRef.current?.setCustomValidity(invalid ? validationMessage : '') + }, [invalid]) + + const handleDateInput = (event: FormEvent) => { + const value = event.currentTarget.value + setDateValue(value) + const next = editableDateFromAnchor(`date:${value}`) + if (!next || clampEditableEventEnd(next).getTime() !== next.getTime()) { + setInvalid(true) + return + } + beginEdit() + applyEnd(next) + } + + const handleDateKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !invalid) commitEdit() + if (event.key === 'Escape') cancelEdit() + } + + const handlePointerCancel = (_event: PointerEvent) => { + cancelEdit() + } + + const minDate = editableDateKey(editableEventEndValues[0]!) + + const maxDate = editableDateKey(editableEventEndValues.at(-1)!) + + const eventDescriptions = editableEvents(input.revision, state.end).map( + (row) => + `${row.label}: ${editableDateKey(row.start)} to ${editableDateKey(row.end)}`, + ) + + return ( +
+ +
+ { + sceneRef.current = scene + }} + /> +
+
+
+ + {editableSummaryText(state.end)} + + + +
+
    + {eventDescriptions.map((description) => ( +
  • {description}
  • + ))} +
+
+
+ ) +} diff --git a/benchmarks/conformance/cases/92-editable-event-range/tanstack.test.ts b/benchmarks/conformance/cases/92-editable-event-range/tanstack.test.ts index 8080ddaf..e7a1a945 100644 --- a/benchmarks/conformance/cases/92-editable-event-range/tanstack.test.ts +++ b/benchmarks/conformance/cases/92-editable-event-range/tanstack.test.ts @@ -119,7 +119,7 @@ describe('definition-owned editable event handle', () => { process.cwd(), 'benchmarks/conformance/cases/92-editable-event-range', ) - const source = readFileSync(resolve(directory, 'tanstack.ts'), 'utf8') + const source = readFileSync(resolve(directory, 'example.tsx'), 'utf8') const view = readFileSync(resolve(directory, 'view.tsx'), 'utf8') expect(existsSync(resolve(directory, 'overlay.ts'))).toBe(true) diff --git a/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts b/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts index 54ea79fb..bf31236c 100644 --- a/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts +++ b/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts @@ -1,26 +1,31 @@ -import { defineChart, rect, text } from '@tanstack/charts' -import { handleX } from '@tanstack/charts/interaction/handle' -import { controlledSignal } from '@tanstack/charts/interaction/signal' -import { scaleBand, scaleUtc } from 'd3-scale' +import { + editableEventDefinition, + editableSummaryText, + editableAriaLabel, + cloneDate, +} from './example' +import type { EditableChartInput, EditableState } from './example' +export { + editableEventDefinition, + editableSummaryText, + editableAriaLabel, + cloneDate, +} from './example' +export type { EditableChartInput, EditableState } from './example' import { editableEventColor } from './colors' import { - clampEditableEventEnd, editableDateFromAnchor, editableDateKey, editableDurationDays, - editableEventEndValues, } from './model' import { - editableDomain, editableEvents, editableEventStart, - editableLanes, initialEditableEventEnd, } from './scenario' import { scenePointToClient } from '../../shared/driver-geometry' import { tanstackCase } from '../../shared/mount' import type { ChartScene } from '@tanstack/charts' -import type { HandleXChange } from '@tanstack/charts/interaction/handle' import type { EditableEvent } from './scenario' import type { ConformanceGeometryQuery, @@ -30,140 +35,10 @@ import type { ConformanceTestDriver, } from '../../types' -export interface EditableChartInput extends ConformanceInput { - end: Date -} - -export interface EditableState { - end: Date - editing: boolean - editCount: number - originEnd: Date | null -} +export { default as Example } from './example' -const margin = { top: 96, right: 26, bottom: 48, left: 82 } const handleId = 'release-end' -export function editableEventDefinition( - input: EditableChartInput, - onEndChange: (value: Date, reason: HandleXChange) => void, -) { - const rows = editableEvents(input.revision, input.end) - const outsideLabels = rows - .filter((row) => row.id !== 'release') - .map((row) => ({ ...row, labelDate: row.end })) - - return defineChart(({ width }) => { - const releaseLabels = rows - .filter( - (row) => - row.id === 'release' && eventBarCanFitLabel(row, width, 'Release'), - ) - .map((row) => ({ - ...row, - labelDate: row.start, - shortLabel: 'Release', - })) - return { - marks: [ - rect(rows, { - id: 'event-ranges', - x1: 'start', - x2: 'end', - y: 'lane', - color: 'id', - radius: 5, - stroke: '#ffffff', - strokeWidth: 1, - }), - ...(input.preview === true - ? [] - : [ - text(outsideLabels, { - id: 'event-labels', - x: 'labelDate', - y: 'lane', - text: 'label', - anchor: 'start', - dx: 5, - fill: 'currentColor', - fontSize: 10, - fontWeight: 600, - }), - text(releaseLabels, { - id: 'release-label', - x: 'labelDate', - y: 'lane', - text: 'shortLabel', - anchor: 'start', - dx: 5, - fill: '#431407', - fontSize: 10, - fontWeight: 700, - }), - ]), - ], - x: { - scale: scaleUtc().domain(editableDomain), - grid: true, - axis: { - ticks: { - format: (value: Date) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), - }, - }, - }, - y: { - scale: scaleBand() - .domain(editableLanes) - .paddingInner(0.38) - .paddingOuter(0.19), - grid: false, - }, - color: { - domain: ['discovery', 'design', 'campaign', 'release'], - range: [ - editableEventColor('discovery'), - editableEventColor('design'), - editableEventColor('campaign'), - editableEventColor('release'), - ], - }, - controls: [ - handleX({ - id: handleId, - value: controlledSignal>( - input.end, - (next, { reason }) => onEndChange(next, reason), - ), - values: editableEventEndValues, - cross: { value: 'Engineering' }, - trackStyle: { - fill: 'color-mix(in srgb, var(--ts-chart-2, #f97316) 58%, transparent)', - }, - ruleStyle: false, - handleStyle: { - fill: 'var(--ts-chart-2, #f97316)', - stroke: 'Canvas', - strokeWidth: 2, - }, - hitSize: 44, - ariaLabel: 'Release end handle', - format: (value) => editableHandleValueText(value), - }), - ], - svgAnimation: false, - keyboard: false, - focusRing: false, - margin, - } - }) -} - export const catalogCase = tanstackCase( (input: ConformanceInput) => editableEventDefinition( @@ -347,44 +222,3 @@ function elementCenter(element: HTMLElement | SVGElement) { focusElement: element, } } - -function editableHandleValueText(end: Date) { - return `Release: ${editableDateKey(editableEventStart)} → ${editableDateKey(end)} · ${editableDurationDays(editableEventStart, end)} days` -} - -export function editableSummaryText(end: Date) { - return `Release · ${compactDate(editableEventStart)} → ${compactDate(end)} · ${editableDurationDays(editableEventStart, end)} days` -} - -function compactDate(date: Date) { - return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }) -} - -export function editableAriaLabel(revision: number, end: Date) { - return `Editable schedule. ${editableEvents(revision, end) - .map( - (row) => - `${row.label}, ${editableDateKey(row.start)} to ${editableDateKey(row.end)}`, - ) - .join('. ')}.` -} - -function eventBarCanFitLabel( - event: EditableEvent, - width: number, - label: string, -) { - const plotWidth = Math.max(0, width - margin.left - margin.right) - const domainWidth = editableDomain[1].getTime() - editableDomain[0].getTime() - const eventWidth = event.end.getTime() - event.start.getTime() - const barWidth = domainWidth > 0 ? (eventWidth / domainWidth) * plotWidth : 0 - return barWidth >= label.length * 6 + 10 -} - -export function cloneDate(date: Date) { - return new Date(date.getTime()) -} diff --git a/benchmarks/conformance/cases/92-editable-event-range/view.tsx b/benchmarks/conformance/cases/92-editable-event-range/view.tsx index c1b2a718..750a8502 100644 --- a/benchmarks/conformance/cases/92-editable-event-range/view.tsx +++ b/benchmarks/conformance/cases/92-editable-event-range/view.tsx @@ -16,20 +16,20 @@ import { editableEventEndValues, } from './model' import { editableEvents, initialEditableEventEnd } from './scenario' +import { createDriver } from './tanstack' import { cloneDate, - createDriver, editableAriaLabel, editableEventDefinition, editableSummaryText, -} from './tanstack' +} from './example' import type { ChartScene } from '@tanstack/charts' import type { HandleXChange } from '@tanstack/charts/interaction/handle' import type { FormEvent, KeyboardEvent, PointerEvent } from 'react' import type { ReactConformanceProps } from '../../shared/react-mount' import type { ConformanceTestDriver } from '../../types' import type { EditableEvent } from './scenario' -import type { EditableState } from './tanstack' +import type { EditableState } from './example' const validationMessage = 'Choose a release end date within the range.' diff --git a/benchmarks/conformance/cases/93-labeled-pie/example.tsx b/benchmarks/conformance/cases/93-labeled-pie/example.tsx new file mode 100644 index 00000000..50153ebd --- /dev/null +++ b/benchmarks/conformance/cases/93-labeled-pie/example.tsx @@ -0,0 +1,91 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { + pie, + polar, + radialArc, + radialRule, + radialText, +} from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { scaleLinear } from 'd3-scale' +import { selectLabeledPieData } from './selection' + +const tau = Math.PI * 2 +const radiusRatio = 0.56 +const labelOffset = 20 +const colors = ['#2563eb', '#7c3aed', '#db2777', '#f59e0b'] + +export const labeledPieDefinition = (input: ExampleOptions) => { + const arcs = pie(selectLabeledPieData(alphabet, input.revision), { + value: 'frequency', + }) + + return defineChart({ + marks: [ + polar({ + radiusRatio, + angle: { scale: scaleLinear().domain([0, tau]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialArc(arcs, { + id: 'letter-slices', + key: 'letter', + color: 'letter', + }), + radialRule(arcs, { + id: 'letter-leaders', + angle: 'angle', + radius1: 1, + radius2: 1, + radius2Offset: labelOffset, + key: 'letter', + stroke: '#94a3b8', + strokeWidth: 1, + }), + radialText(arcs, { + id: 'letter-labels', + angle: 'angle', + radius: 1, + radiusOffset: labelOffset, + text: 'letter', + key: 'letter', + color: 'letter', + fontSize: 12, + fontWeight: 500, + anchor: 'outside', + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Letter frequency pie with labels' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(labeledPieDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/93-labeled-pie/tanstack.test.ts b/benchmarks/conformance/cases/93-labeled-pie/tanstack.test.ts index eaadd55b..2c6b252b 100644 --- a/benchmarks/conformance/cases/93-labeled-pie/tanstack.test.ts +++ b/benchmarks/conformance/cases/93-labeled-pie/tanstack.test.ts @@ -135,7 +135,7 @@ describe('native labeled pie composition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/93-labeled-pie/tanstack.ts', + 'benchmarks/conformance/cases/93-labeled-pie/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/93-labeled-pie/tanstack.ts b/benchmarks/conformance/cases/93-labeled-pie/tanstack.ts index 8b50fc27..6c029e65 100644 --- a/benchmarks/conformance/cases/93-labeled-pie/tanstack.ts +++ b/benchmarks/conformance/cases/93-labeled-pie/tanstack.ts @@ -1,72 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { - pie, - polar, - radialArc, - radialRule, - radialText, -} from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { scaleLinear } from 'd3-scale' -import { selectLabeledPieData } from './selection' -import { tanstackCase } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const tau = Math.PI * 2 -const radiusRatio = 0.56 -const labelOffset = 20 -const colors = ['#2563eb', '#7c3aed', '#db2777', '#f59e0b'] +export * from './example' -export const labeledPieDefinition = (input: ConformanceInput) => { - const arcs = pie(selectLabeledPieData(alphabet, input.revision), { - value: 'frequency', - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio, - angle: { scale: scaleLinear().domain([0, tau]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialArc(arcs, { - id: 'letter-slices', - key: 'letter', - color: 'letter', - }), - radialRule(arcs, { - id: 'letter-leaders', - angle: 'angle', - radius1: 1, - radius2: 1, - radius2Offset: labelOffset, - key: 'letter', - stroke: '#94a3b8', - strokeWidth: 1, - }), - radialText(arcs, { - id: 'letter-labels', - angle: 'angle', - radius: 1, - radiusOffset: labelOffset, - text: 'letter', - key: 'letter', - color: 'letter', - fontSize: 12, - fontWeight: 500, - anchor: 'outside', - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const catalogCase = tanstackCase( - labeledPieDefinition, - 'Letter frequency pie with labels', -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/94-center-donut/example.tsx b/benchmarks/conformance/cases/94-center-donut/example.tsx new file mode 100644 index 00000000..3fc826fb --- /dev/null +++ b/benchmarks/conformance/cases/94-center-donut/example.tsx @@ -0,0 +1,87 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc, radialText } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { scaleLinear } from 'd3-scale' +import { selectCenterDonutData } from './selection' + +const tau = Math.PI * 2 +const colors = ['#0ea5e9', '#6366f1', '#a855f7'] +const percentage = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 1, +}) + +export const centerDonutDefinition = (input: ExampleOptions) => { + const data = selectCenterDonutData(alphabet, input.revision) + const arcs = pie(data, { value: 'frequency' }) + const total = data.reduce((sum, row) => sum + row.frequency, 0) + const center = [ + { id: 'total', angle: 0, radius: 0, text: percentage.format(total) }, + ] + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.8, + angle: { scale: scaleLinear().domain([0, tau]) }, + radius: { scale: scaleLinear().domain([0, 1]) }, + marks: [ + radialArc(arcs, { + id: 'letter-slices', + key: 'letter', + innerRadius: ({ radius }) => radius * 0.62, + color: 'letter', + }), + radialText(center, { + id: 'center-total', + angle: 'angle', + radius: 'radius', + key: 'id', + text: 'text', + fill: '#0f172a', + fontSize: 20, + fontWeight: 700, + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Letter frequency donut with total' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(centerDonutDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + 'letter' in datum + ? `${datum.letter} · ${percentage.format(datum.frequency)}` + : `Total · ${datum.text}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/94-center-donut/tanstack.test.ts b/benchmarks/conformance/cases/94-center-donut/tanstack.test.ts index c87831ab..9e91b397 100644 --- a/benchmarks/conformance/cases/94-center-donut/tanstack.test.ts +++ b/benchmarks/conformance/cases/94-center-donut/tanstack.test.ts @@ -132,7 +132,7 @@ describe('native center-donut allocation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/94-center-donut/tanstack.ts', + 'benchmarks/conformance/cases/94-center-donut/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/94-center-donut/tanstack.ts b/benchmarks/conformance/cases/94-center-donut/tanstack.ts index 3aced7cf..6c029e65 100644 --- a/benchmarks/conformance/cases/94-center-donut/tanstack.ts +++ b/benchmarks/conformance/cases/94-center-donut/tanstack.ts @@ -1,64 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc, radialText } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { scaleLinear } from 'd3-scale' -import { selectCenterDonutData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const tau = Math.PI * 2 -const colors = ['#0ea5e9', '#6366f1', '#a855f7'] -const percentage = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 1, -}) +export * from './example' -export const centerDonutDefinition = (input: ConformanceInput) => { - const data = selectCenterDonutData(alphabet, input.revision) - const arcs = pie(data, { value: 'frequency' }) - const total = data.reduce((sum, row) => sum + row.frequency, 0) - const center = [ - { id: 'total', angle: 0, radius: 0, text: percentage.format(total) }, - ] +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.8, - angle: { scale: scaleLinear().domain([0, tau]) }, - radius: { scale: scaleLinear().domain([0, 1]) }, - marks: [ - radialArc(arcs, { - id: 'letter-slices', - key: 'letter', - innerRadius: ({ radius }) => radius * 0.62, - color: 'letter', - }), - radialText(center, { - id: 'center-total', - angle: 'angle', - radius: 'radius', - key: 'id', - text: 'text', - fill: '#0f172a', - fontSize: 20, - fontWeight: 700, - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - centerDonutDefinition, - 'Letter frequency donut with total', - { - format: ({ datum }) => - 'letter' in datum - ? `${datum.letter} · ${percentage.format(datum.frequency)}` - : `Total · ${datum.text}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/95-rounded-donut/example.tsx b/benchmarks/conformance/cases/95-rounded-donut/example.tsx new file mode 100644 index 00000000..d9aa8205 --- /dev/null +++ b/benchmarks/conformance/cases/95-rounded-donut/example.tsx @@ -0,0 +1,71 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { selectRoundedDonutData } from './selection' + +const gapAngle = (Math.PI / 180) * 3 +const colors = ['#0284c7', '#4f46e5', '#9333ea', '#db2777', '#ea580c'] +const percentage = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 2, +}) + +export const roundedDonutDefinition = (input: ExampleOptions) => { + const arcs = pie(selectRoundedDonutData(alphabet, input.revision), { + value: 'frequency', + gapAngle, + }) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.8, + marks: [ + radialArc(arcs, { + id: 'letter-slices', + key: 'letter', + innerRadius: ({ radius }) => radius * 0.58, + cornerRadius: 8, + color: 'letter', + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Rounded letter frequency donut' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(roundedDonutDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.letter} · ${percentage.format(datum.frequency)}`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/95-rounded-donut/tanstack.test.ts b/benchmarks/conformance/cases/95-rounded-donut/tanstack.test.ts index 112c4b85..d27b437c 100644 --- a/benchmarks/conformance/cases/95-rounded-donut/tanstack.test.ts +++ b/benchmarks/conformance/cases/95-rounded-donut/tanstack.test.ts @@ -108,7 +108,7 @@ describe('native rounded-donut allocation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/95-rounded-donut/tanstack.ts', + 'benchmarks/conformance/cases/95-rounded-donut/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/95-rounded-donut/tanstack.ts b/benchmarks/conformance/cases/95-rounded-donut/tanstack.ts index 7854c695..6c029e65 100644 --- a/benchmarks/conformance/cases/95-rounded-donut/tanstack.ts +++ b/benchmarks/conformance/cases/95-rounded-donut/tanstack.ts @@ -1,48 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { selectRoundedDonutData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const gapAngle = (Math.PI / 180) * 3 -const colors = ['#0284c7', '#4f46e5', '#9333ea', '#db2777', '#ea580c'] -const percentage = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 2, -}) +export * from './example' -export const roundedDonutDefinition = (input: ConformanceInput) => { - const arcs = pie(selectRoundedDonutData(alphabet, input.revision), { - value: 'frequency', - gapAngle, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.8, - marks: [ - radialArc(arcs, { - id: 'letter-slices', - key: 'letter', - innerRadius: ({ radius }) => radius * 0.58, - cornerRadius: 8, - color: 'letter', - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - roundedDonutDefinition, - 'Rounded letter frequency donut', - { - format: ({ datum }) => - `${datum.letter} · ${percentage.format(datum.frequency)}`, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/96-nested-donut/example.tsx b/benchmarks/conformance/cases/96-nested-donut/example.tsx new file mode 100644 index 00000000..a7cf2425 --- /dev/null +++ b/benchmarks/conformance/cases/96-nested-donut/example.tsx @@ -0,0 +1,84 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { flare } from '@charts-poc/demo-data/flare' +import { defineChart } from '@tanstack/charts' +import { pie, polar, radialArc } from '@tanstack/charts/polar' +import { nestedFlareDonut } from './transform' + +const names = [ + 'flare.animate', + 'flare.data', + 'flare.animate.core', + 'flare.animate.interpolate', + 'flare.data.core', + 'flare.data.converters', +] +const colors = [ + '#38bdf8', + '#8b5cf6', + '#0284c7', + '#0ea5e9', + '#7c3aed', + '#a855f7', +] + +export const nestedDonutDefinition = (input: ExampleOptions) => { + const sourceRows = + input.revision % 2 === 0 + ? flare + : flare.filter((row) => row.size === null || row.size >= 1_000) + const data = nestedFlareDonut(sourceRows) + const innerArcs = pie(data.inner, { value: 'size' }) + const outerArcs = pie(data.outer, { value: 'size' }) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.8, + marks: [ + radialArc(innerArcs, { + id: 'family-slices', + key: 'name', + innerRadius: ({ radius }) => radius * 0.12, + outerRadius: ({ radius }) => radius * 0.46, + color: 'name', + }), + radialArc(outerArcs, { + id: 'detail-slices', + key: 'name', + innerRadius: ({ radius }) => radius * 0.56, + color: 'name', + }), + ], + }), + ], + color: { domain: names, range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Nested Flare package sizes' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(nestedDonutDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/96-nested-donut/tanstack.test.ts b/benchmarks/conformance/cases/96-nested-donut/tanstack.test.ts index 11ecbba0..fda69c10 100644 --- a/benchmarks/conformance/cases/96-nested-donut/tanstack.test.ts +++ b/benchmarks/conformance/cases/96-nested-donut/tanstack.test.ts @@ -127,7 +127,7 @@ describe('native nested-donut allocation', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/96-nested-donut/tanstack.ts', + 'benchmarks/conformance/cases/96-nested-donut/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/96-nested-donut/tanstack.ts b/benchmarks/conformance/cases/96-nested-donut/tanstack.ts index 8a001a5d..6c029e65 100644 --- a/benchmarks/conformance/cases/96-nested-donut/tanstack.ts +++ b/benchmarks/conformance/cases/96-nested-donut/tanstack.ts @@ -1,63 +1,8 @@ -import { flare } from '@charts-poc/demo-data/flare' -import { defineChart } from '@tanstack/charts' -import { pie, polar, radialArc } from '@tanstack/charts/polar' -import { nestedFlareDonut } from './transform' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const names = [ - 'flare.animate', - 'flare.data', - 'flare.animate.core', - 'flare.animate.interpolate', - 'flare.data.core', - 'flare.data.converters', -] -const colors = [ - '#38bdf8', - '#8b5cf6', - '#0284c7', - '#0ea5e9', - '#7c3aed', - '#a855f7', -] +export * from './example' -export const nestedDonutDefinition = (input: ConformanceInput) => { - const sourceRows = - input.revision % 2 === 0 - ? flare - : flare.filter((row) => row.size === null || row.size >= 1_000) - const data = nestedFlareDonut(sourceRows) - const innerArcs = pie(data.inner, { value: 'size' }) - const outerArcs = pie(data.outer, { value: 'size' }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.8, - marks: [ - radialArc(innerArcs, { - id: 'family-slices', - key: 'name', - innerRadius: ({ radius }) => radius * 0.12, - outerRadius: ({ radius }) => radius * 0.46, - color: 'name', - }), - radialArc(outerArcs, { - id: 'detail-slices', - key: 'name', - innerRadius: ({ radius }) => radius * 0.56, - color: 'name', - }), - ], - }), - ], - color: { domain: names, range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - nestedDonutDefinition, - 'Nested Flare package sizes', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/97-rose/example.tsx b/benchmarks/conformance/cases/97-rose/example.tsx new file mode 100644 index 00000000..e0d316ac --- /dev/null +++ b/benchmarks/conformance/cases/97-rose/example.tsx @@ -0,0 +1,73 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart } from '@tanstack/charts' +import { polar, radialBarRadius } from '@tanstack/charts/polar' +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { scaleBand, scaleLinear } from 'd3-scale' +import { selectRoseData } from './selection' + +const colors = [ + '#0369a1', + '#2563eb', + '#4f46e5', + '#7c3aed', + '#c026d3', + '#db2777', +] +const maximumFrequency = alphabet[0]?.frequency ?? 1 + +export const roseDefinition = (input: ExampleOptions) => { + const data = selectRoseData(alphabet, input.revision) + + return defineChart({ + marks: [ + polar({ + radiusRatio: 0.8, + angle: { scale: () => scaleBand() }, + radius: { + scale: scaleLinear().domain([0, maximumFrequency]), + range: [({ radius }) => radius * 0.3, ({ radius }) => radius], + }, + marks: [ + radialBarRadius(data, { + id: 'letter-bars', + angle: 'letter', + radius: 'frequency', + key: 'letter', + color: 'letter', + stroke: '#ffffff', + strokeWidth: 1, + }), + ], + }), + ], + color: { range: colors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'English letter frequency rose' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(roseDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/97-rose/tanstack.test.ts b/benchmarks/conformance/cases/97-rose/tanstack.test.ts index 64f3d3cf..4efa55b9 100644 --- a/benchmarks/conformance/cases/97-rose/tanstack.test.ts +++ b/benchmarks/conformance/cases/97-rose/tanstack.test.ts @@ -146,7 +146,7 @@ describe('native radius-extending rose bars', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/97-rose/tanstack.ts', + 'benchmarks/conformance/cases/97-rose/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/97-rose/tanstack.ts b/benchmarks/conformance/cases/97-rose/tanstack.ts index 466aaf9b..6c029e65 100644 --- a/benchmarks/conformance/cases/97-rose/tanstack.ts +++ b/benchmarks/conformance/cases/97-rose/tanstack.ts @@ -1,52 +1,8 @@ -import { defineChart } from '@tanstack/charts' -import { polar, radialBarRadius } from '@tanstack/charts/polar' -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { scaleBand, scaleLinear } from 'd3-scale' -import { selectRoseData } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const colors = [ - '#0369a1', - '#2563eb', - '#4f46e5', - '#7c3aed', - '#c026d3', - '#db2777', -] -const maximumFrequency = alphabet[0]?.frequency ?? 1 +export * from './example' -export const roseDefinition = (input: ConformanceInput) => { - const data = selectRoseData(alphabet, input.revision) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - radiusRatio: 0.8, - angle: { scale: () => scaleBand() }, - radius: { - scale: scaleLinear().domain([0, maximumFrequency]), - range: [({ radius }) => radius * 0.3, ({ radius }) => radius], - }, - marks: [ - radialBarRadius(data, { - id: 'letter-bars', - angle: 'letter', - radius: 'frequency', - key: 'letter', - color: 'letter', - stroke: '#ffffff', - strokeWidth: 1, - }), - ], - }), - ], - color: { range: colors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - roseDefinition, - 'English letter frequency rose', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/98-needle-gauge/example.tsx b/benchmarks/conformance/cases/98-needle-gauge/example.tsx new file mode 100644 index 00000000..fef3e3ca --- /dev/null +++ b/benchmarks/conformance/cases/98-needle-gauge/example.tsx @@ -0,0 +1,136 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { usCountyUnemployment } from '@charts-poc/demo-data/us-county-unemployment' +import { defineChart } from '@tanstack/charts' +import { + pie, + polar, + radialArc, + radialDot, + radialRule, + radialText, +} from '@tanstack/charts/polar' +import { scaleLinear } from 'd3-scale' +import { + gaugeBands, + gaugeMaximum, + gaugeTicks, + type GaugeBand, +} from './transform' + +const startAngle = -Math.PI / 2 +const endAngle = Math.PI / 2 +const angleScale = scaleLinear().domain([0, gaugeMaximum]) +const radiusScale = scaleLinear().domain([0, 1]) +const arcs = pie(gaugeBands, { + value: 'value', + startAngle, + endAngle, +}) +const bandIds: readonly GaugeBand['id'][] = ['low', 'elevated', 'high'] +const bandColors = ['#22c55e', '#f59e0b', '#ef4444'] + +export const needleGaugeDefinition = (input: ExampleOptions) => { + const reading = usCountyUnemployment[(input.revision % 2) * 2] + if (!reading) { + throw new Error('County unemployment data is incomplete.') + } + + return defineChart({ + marks: [ + polar({ + angle: { scale: angleScale }, + radius: { scale: radiusScale }, + startAngle, + endAngle, + radiusRatio: 0.82, + marks: [ + radialArc(arcs, { + id: 'gauge-bands', + key: 'id', + innerRadius: ({ radius }) => radius * 0.72, + color: 'id', + }), + radialRule(gaugeTicks, { + id: 'gauge-ticks', + key: 'id', + angle: 'value', + radius1: 0.76, + radius2: 0.94, + stroke: '#ffffff', + strokeOpacity: 0.85, + strokeWidth: 2, + }), + radialRule([reading], { + id: 'gauge-needle', + key: 'id', + angle: 'rate', + radius1: 0, + radius2: 0.64, + stroke: 'currentColor', + strokeWidth: 4, + }), + radialDot([reading], { + id: 'gauge-hub', + key: 'id', + angle: 'rate', + radius: 0, + r: 8, + fill: 'currentColor', + }), + radialText([reading], { + id: 'gauge-value', + key: 'id', + angle: 'rate', + radius: 0, + text: (row) => `${row.rate}%`, + dy: 34, + anchor: 'middle', + baseline: 'middle', + fill: 'currentColor', + fontSize: 18, + fontWeight: 700, + }), + ], + }), + ], + color: { domain: bandIds, range: bandColors }, + margin: 0, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'County unemployment gauge' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(needleGaugeDefinition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => { + if ('label' in datum) { + return `${datum.label} · ${datum.value} percentage-point band` + } + return `${datum.county}, ${datum.state} · ${datum.rate}% unemployment` + }, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/98-needle-gauge/tanstack.test.ts b/benchmarks/conformance/cases/98-needle-gauge/tanstack.test.ts index 8f677cbd..2ae8ade2 100644 --- a/benchmarks/conformance/cases/98-needle-gauge/tanstack.test.ts +++ b/benchmarks/conformance/cases/98-needle-gauge/tanstack.test.ts @@ -181,7 +181,7 @@ describe('native needle-gauge composition', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/98-needle-gauge/tanstack.ts', + 'benchmarks/conformance/cases/98-needle-gauge/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/98-needle-gauge/tanstack.ts b/benchmarks/conformance/cases/98-needle-gauge/tanstack.ts index e513b117..6c029e65 100644 --- a/benchmarks/conformance/cases/98-needle-gauge/tanstack.ts +++ b/benchmarks/conformance/cases/98-needle-gauge/tanstack.ts @@ -1,113 +1,8 @@ -import { usCountyUnemployment } from '@charts-poc/demo-data/us-county-unemployment' -import { defineChart } from '@tanstack/charts' -import { - pie, - polar, - radialArc, - radialDot, - radialRule, - radialText, -} from '@tanstack/charts/polar' -import { scaleLinear } from 'd3-scale' -import { - gaugeBands, - gaugeMaximum, - gaugeTicks, - type GaugeBand, -} from './transform' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const startAngle = -Math.PI / 2 -const endAngle = Math.PI / 2 -const angleScale = scaleLinear().domain([0, gaugeMaximum]) -const radiusScale = scaleLinear().domain([0, 1]) -const arcs = pie(gaugeBands, { - value: 'value', - startAngle, - endAngle, -}) -const bandIds: readonly GaugeBand['id'][] = ['low', 'elevated', 'high'] -const bandColors = ['#22c55e', '#f59e0b', '#ef4444'] +export * from './example' -export const needleGaugeDefinition = (input: ConformanceInput) => { - const reading = usCountyUnemployment[(input.revision % 2) * 2] - if (!reading) { - throw new Error('County unemployment data is incomplete.') - } +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - polar({ - angle: { scale: angleScale }, - radius: { scale: radiusScale }, - startAngle, - endAngle, - radiusRatio: 0.82, - marks: [ - radialArc(arcs, { - id: 'gauge-bands', - key: 'id', - innerRadius: ({ radius }) => radius * 0.72, - color: 'id', - }), - radialRule(gaugeTicks, { - id: 'gauge-ticks', - key: 'id', - angle: 'value', - radius1: 0.76, - radius2: 0.94, - stroke: '#ffffff', - strokeOpacity: 0.85, - strokeWidth: 2, - }), - radialRule([reading], { - id: 'gauge-needle', - key: 'id', - angle: 'rate', - radius1: 0, - radius2: 0.64, - stroke: 'currentColor', - strokeWidth: 4, - }), - radialDot([reading], { - id: 'gauge-hub', - key: 'id', - angle: 'rate', - radius: 0, - r: 8, - fill: 'currentColor', - }), - radialText([reading], { - id: 'gauge-value', - key: 'id', - angle: 'rate', - radius: 0, - text: (row) => `${row.rate}%`, - dy: 34, - anchor: 'middle', - baseline: 'middle', - fill: 'currentColor', - fontSize: 18, - fontWeight: 700, - }), - ], - }), - ], - color: { domain: bandIds, range: bandColors }, - margin: 0, - }) -} - -export const mount = tanstackMount( - needleGaugeDefinition, - 'County unemployment gauge', - { - format: ({ datum }) => { - if ('label' in datum) { - return `${datum.label} · ${datum.value} percentage-point band` - } - return `${datum.county}, ${datum.state} · ${datum.rate}% unemployment` - }, - }, -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/99-comparative-radar/example.tsx b/benchmarks/conformance/cases/99-comparative-radar/example.tsx new file mode 100644 index 00000000..49069af8 --- /dev/null +++ b/benchmarks/conformance/cases/99-comparative-radar/example.tsx @@ -0,0 +1,134 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { defineChart, normalize, select } from '@tanstack/charts' +import { fold } from '@tanstack/charts/transform/fold' +import { + angleGrid, + polar, + radialArea, + radialGrid, +} from '@tanstack/charts/polar' +import { decathlon } from '@charts-poc/demo-data/decathlon' +import { scaleLinear, scalePoint } from 'd3-scale' +import { curveLinearClosed } from 'd3-shape' +import { radarCountries, radarEvents, timedEvents } from './selection' +import type { PolarGuideLabelContext } from '@tanstack/charts/polar' + +const ringValues = [0.2, 0.4, 0.6, 0.8, 1] as const +const angleScale = scalePoint().domain(radarEvents) +const radiusScale = scaleLinear().domain([0, 1]) +const colors = ['#7c3aed', '#0ea5e9'] +const selectedCountryNames: ReadonlySet = new Set(radarCountries) + +export const foldedDecathlon = fold(decathlon, { + fields: radarEvents, + as: { key: 'event', value: 'result' }, +}) +export const normalizedDecathlon = normalize(foldedDecathlon, { + by: 'event', + value: (datum) => + timedEvents.has(datum.event) ? -datum.result : datum.result, + basis: 'extent', + as: 'relativePerformance', +}) +const selectedCountries = normalizedDecathlon.filter(({ Country }) => + selectedCountryNames.has(Country), +) +export const radarProfiles = select(selectedCountries, { + by: { Country: 'Country', event: 'event' }, + select: 'first', +}) + +function angleLabelIsTopOrBottom(angle: number): boolean { + return Math.abs(Math.sin(angle)) <= Math.SQRT1_2 +} + +function angleLabelDy({ angle, y }: PolarGuideLabelContext): number { + if (!angleLabelIsTopOrBottom(angle)) return 1.1 + return y > 0 ? -1.1 : 0 +} + +export const comparativeRadarDefinition = (input: ExampleOptions) => { + const margin = + input.width < 480 + ? { top: 20, right: 55, bottom: 20, left: 105 } + : { top: 20, right: 20, bottom: 20, left: 20 } + + return defineChart({ + marks: [ + polar({ + angle: { scale: angleScale, wrap: true }, + radius: { scale: radiusScale }, + inset: 0, + radiusRatio: input.preview === true ? 0.94 : 0.78, + guides: [ + radialGrid({ + values: ringValues, + shape: 'polygon', + labels: input.preview !== true, + labelAngle: Math.PI / 3, + labelRotate: 60, + labelBaseline: 'auto', + labelFontSize: 12, + format: (value) => String(Number(value) * 100), + labelFill: '#cccccc', + stroke: '#cbd5e1', + }), + angleGrid({ + values: radarEvents, + labels: input.preview !== true, + labelOffset: 8, + labelDy: angleLabelDy, + labelFill: '#808080', + labelFontSize: 12, + format: String, + stroke: '#cbd5e1', + }), + ], + marks: [ + radialArea(radarProfiles, { + id: 'country-profiles', + angle: 'event', + radius: 'relativePerformance', + color: 'Country', + key: ({ Country, event }) => `${Country}:${event}`, + className: 'ts-chart__radar', + curve: curveLinearClosed, + fillOpacity: 0.18, + }), + ], + }), + ], + color: { + domain: radarCountries, + range: colors, + }, + margin: input.preview === true ? 0 : margin, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Comparative radar chart' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(comparativeRadarDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/99-comparative-radar/tanstack.test.ts b/benchmarks/conformance/cases/99-comparative-radar/tanstack.test.ts index f9776c28..2c175bc9 100644 --- a/benchmarks/conformance/cases/99-comparative-radar/tanstack.test.ts +++ b/benchmarks/conformance/cases/99-comparative-radar/tanstack.test.ts @@ -94,7 +94,7 @@ describe('folded comparative radar profiles', () => { const source = readFileSync( resolve( process.cwd(), - 'benchmarks/conformance/cases/99-comparative-radar/tanstack.ts', + 'benchmarks/conformance/cases/99-comparative-radar/example.tsx', ), 'utf8', ) diff --git a/benchmarks/conformance/cases/99-comparative-radar/tanstack.ts b/benchmarks/conformance/cases/99-comparative-radar/tanstack.ts index 0cf54703..6c029e65 100644 --- a/benchmarks/conformance/cases/99-comparative-radar/tanstack.ts +++ b/benchmarks/conformance/cases/99-comparative-radar/tanstack.ts @@ -1,113 +1,8 @@ -import { defineChart, normalize, select } from '@tanstack/charts' -import { fold } from '@tanstack/charts/transform/fold' -import { - angleGrid, - polar, - radialArea, - radialGrid, -} from '@tanstack/charts/polar' -import { decathlon } from '@charts-poc/demo-data/decathlon' -import { scaleLinear, scalePoint } from 'd3-scale' -import { curveLinearClosed } from 'd3-shape' -import { radarCountries, radarEvents, timedEvents } from './selection' -import { tanstackMount } from '../../shared/mount' -import type { PolarGuideLabelContext } from '@tanstack/charts/polar' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const ringValues = [0.2, 0.4, 0.6, 0.8, 1] as const -const angleScale = scalePoint().domain(radarEvents) -const radiusScale = scaleLinear().domain([0, 1]) -const colors = ['#7c3aed', '#0ea5e9'] -const selectedCountryNames: ReadonlySet = new Set(radarCountries) +export * from './example' -export const foldedDecathlon = fold(decathlon, { - fields: radarEvents, - as: { key: 'event', value: 'result' }, -}) -export const normalizedDecathlon = normalize(foldedDecathlon, { - by: 'event', - value: (datum) => - timedEvents.has(datum.event) ? -datum.result : datum.result, - basis: 'extent', - as: 'relativePerformance', -}) -const selectedCountries = normalizedDecathlon.filter(({ Country }) => - selectedCountryNames.has(Country), -) -export const radarProfiles = select(selectedCountries, { - by: { Country: 'Country', event: 'event' }, - select: 'first', -}) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -function angleLabelIsTopOrBottom(angle: number): boolean { - return Math.abs(Math.sin(angle)) <= Math.SQRT1_2 -} - -function angleLabelDy({ angle, y }: PolarGuideLabelContext): number { - if (!angleLabelIsTopOrBottom(angle)) return 1.1 - return y > 0 ? -1.1 : 0 -} - -export const comparativeRadarDefinition = (input: ConformanceInput) => { - const margin = - input.width < 480 - ? { top: 20, right: 55, bottom: 20, left: 105 } - : { top: 20, right: 20, bottom: 20, left: 20 } - - return defineChart({ - marks: [ - polar({ - angle: { scale: angleScale, wrap: true }, - radius: { scale: radiusScale }, - inset: 0, - radiusRatio: input.preview === true ? 0.94 : 0.78, - guides: [ - radialGrid({ - values: ringValues, - shape: 'polygon', - labels: input.preview !== true, - labelAngle: Math.PI / 3, - labelRotate: 60, - labelBaseline: 'auto', - labelFontSize: 12, - format: (value) => String(Number(value) * 100), - labelFill: '#cccccc', - stroke: '#cbd5e1', - }), - angleGrid({ - values: radarEvents, - labels: input.preview !== true, - labelOffset: 8, - labelDy: angleLabelDy, - labelFill: '#808080', - labelFontSize: 12, - format: String, - stroke: '#cbd5e1', - }), - ], - marks: [ - radialArea(radarProfiles, { - id: 'country-profiles', - angle: 'event', - radius: 'relativePerformance', - color: 'Country', - key: ({ Country, event }) => `${Country}:${event}`, - className: 'ts-chart__radar', - curve: curveLinearClosed, - fillOpacity: 0.18, - }), - ], - }), - ], - color: { - domain: radarCountries, - range: colors, - }, - margin: input.preview === true ? 0 : margin, - }) -} - -export const mount = tanstackMount( - comparativeRadarDefinition, - 'Comparative radar chart', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/bar-grouped/example.tsx b/benchmarks/conformance/cases/bar-grouped/example.tsx new file mode 100644 index 00000000..5f05a0ea --- /dev/null +++ b/benchmarks/conformance/cases/bar-grouped/example.tsx @@ -0,0 +1,84 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { penguins } from '@charts-poc/demo-data/penguins' +import { + barY, + colorLegend, + defineChart, + group, + groupBy, +} from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' +import type { PenguinsRow } from '@charts-poc/demo-data/penguins' + +const sexDomain = ['FEMALE', 'MALE'] +const sexColors = ['#2563eb', '#f97316'] + +type SexedPenguin = PenguinsRow & { readonly sex: string } + +export const groupedBarDefinition = (input: ExampleOptions) => + defineChart(({ width }) => { + const observations = penguins + .slice(0, penguins.length - input.revision * 12) + .filter((row): row is SexedPenguin => row.sex !== null) + const rows = groupBy(observations, { + by: { species: 'species', sex: 'sex' }, + outputs: { count: { reduce: 'count' } }, + }) + + return { + marks: [ + barY(rows, { + id: 'penguin-count-bars', + x: 'species', + y: 'count', + color: 'sex', + layout: group({ + scale: scaleBand().domain(sexDomain).paddingInner(0.08), + }), + inset: 1, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.06), + axis: { tickLabels: { rotate: width < 640 ? -32 : 0 } }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 5 }, label: 'Penguins' }, + }, + color: { + range: sexColors, + legend: colorLegend({ + label: 'Sex', + }), + }, + } + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Penguins grouped by species' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(groupedBarDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/bar-grouped/tanstack.ts b/benchmarks/conformance/cases/bar-grouped/tanstack.ts index fefe38e8..6c029e65 100644 --- a/benchmarks/conformance/cases/bar-grouped/tanstack.ts +++ b/benchmarks/conformance/cases/bar-grouped/tanstack.ts @@ -1,63 +1,8 @@ -import { penguins } from '@charts-poc/demo-data/penguins' -import { - barY, - colorLegend, - defineChart, - group, - groupBy, -} from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { PenguinsRow } from '@charts-poc/demo-data/penguins' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const sexDomain = ['FEMALE', 'MALE'] -const sexColors = ['#2563eb', '#f97316'] +export * from './example' -type SexedPenguin = PenguinsRow & { readonly sex: string } +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) -export const groupedBarDefinition = (input: ConformanceInput) => - defineChart(({ width }) => { - const observations = penguins - .slice(0, penguins.length - input.revision * 12) - .filter((row): row is SexedPenguin => row.sex !== null) - const rows = groupBy(observations, { - by: { species: 'species', sex: 'sex' }, - outputs: { count: { reduce: 'count' } }, - }) - - return { - marks: [ - barY(rows, { - id: 'penguin-count-bars', - x: 'species', - y: 'count', - color: 'sex', - layout: group({ - scale: scaleBand().domain(sexDomain).paddingInner(0.08), - }), - inset: 1, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.06), - axis: { tickLabels: { rotate: width < 640 ? -32 : 0 } }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 5 }, label: 'Penguins' }, - }, - color: { - range: sexColors, - legend: colorLegend({ - label: 'Sex', - }), - }, - } - }) - -export const mount = tanstackMount( - groupedBarDefinition, - 'Penguins grouped by species', -) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/bar-horizontal-ranking/example.tsx b/benchmarks/conformance/cases/bar-horizontal-ranking/example.tsx new file mode 100644 index 00000000..d58102a7 --- /dev/null +++ b/benchmarks/conformance/cases/bar-horizontal-ranking/example.tsx @@ -0,0 +1,60 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { citywages } from '@charts-poc/demo-data/citywages' +import { barX, defineChart, ruleX } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +export const definition = (input: ExampleOptions) => { + const rows = citywages + .slice(input.revision * 4, input.revision * 4 + 8) + .sort((left, right) => right.POP_2015 - left.POP_2015) + + return defineChart({ + marks: [ + barX(rows, { + x: 'POP_2015', + y: 'Metro', + fill: '#7c3aed', + inset: 1, + }), + ruleX([0]), + ], + x: { + scale: scaleLinear, + grid: input.preview !== true, + axis: + input.preview === true + ? false + : { ticks: { count: 5 }, label: '2015 population' }, + }, + y: { + scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Horizontal ranking with long labels' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts b/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts index 044bf126..6b8596ee 100644 --- a/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts +++ b/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts @@ -1,41 +1,12 @@ -import { citywages } from '@charts-poc/demo-data/citywages' -import { barX, defineChart, ruleX } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => { - const rows = citywages - .slice(input.revision * 4, input.revision * 4 + 8) - .sort((left, right) => right.POP_2015 - left.POP_2015) +export * from './example' - return defineChart({ - marks: [ - barX(rows, { - x: 'POP_2015', - y: 'Metro', - fill: '#7c3aed', - inset: 1, - }), - ruleX([0]), - ], - x: { - scale: scaleLinear, - grid: input.preview !== true, - axis: - input.preview === true - ? false - : { ticks: { count: 5 }, label: '2015 population' }, - }, - y: { - scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - }, - }) -} - -export const mount = tanstackMount( - definition, - 'Horizontal ranking with long labels', - true, +export const mount = tanstackExampleMount( + createExampleChart, + exampleAriaLabel, { guides: true, margin: true }, ) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/bar-stacked/example.tsx b/benchmarks/conformance/cases/bar-stacked/example.tsx new file mode 100644 index 00000000..6c684d43 --- /dev/null +++ b/benchmarks/conformance/cases/bar-stacked/example.tsx @@ -0,0 +1,70 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { crimeanWar } from '@charts-poc/demo-data/crimean-war' +import { barY, defineChart, fold, ruleY, stack } from '@tanstack/charts' +import { scaleLinear, scaleUtc } from 'd3-scale' + +const causes = ['disease', 'wounds', 'other'] as const +const causeColors = ['#4269d0', '#ff725c', '#efb118'] + +export const stackedBarDefinition = (input: ExampleOptions) => { + const rows = fold(crimeanWar.slice(input.revision), { + fields: causes, + as: { key: 'cause', value: 'deaths' }, + }) + + return defineChart({ + marks: [ + barY(rows, { + id: 'death-bars', + x: 'date', + y: 'deaths', + z: 'cause', + color: 'cause', + layout: stack({ order: [...causes].reverse() }), + }), + ruleY([0]), + ], + x: { + scale: scaleUtc, + axis: { ticks: { count: 6, format: (value) => month.format(value) } }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { count: 5 }, label: 'Deaths' }, + }, + color: { domain: causes, range: causeColors }, + }) +} + +const month = new Intl.DateTimeFormat('en-US', { + month: 'short', + timeZone: 'UTC', +}) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Crimean War deaths by cause' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(stackedBarDefinition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/bar-stacked/tanstack.ts b/benchmarks/conformance/cases/bar-stacked/tanstack.ts index 84b4d07d..6c029e65 100644 --- a/benchmarks/conformance/cases/bar-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/bar-stacked/tanstack.ts @@ -1,49 +1,8 @@ -import { crimeanWar } from '@charts-poc/demo-data/crimean-war' -import { barY, defineChart, fold, ruleY, stack } from '@tanstack/charts' -import { scaleLinear, scaleUtc } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const causes = ['disease', 'wounds', 'other'] as const -const causeColors = ['#4269d0', '#ff725c', '#efb118'] +export * from './example' -export const stackedBarDefinition = (input: ConformanceInput) => { - const rows = fold(crimeanWar.slice(input.revision), { - fields: causes, - as: { key: 'cause', value: 'deaths' }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - barY(rows, { - id: 'death-bars', - x: 'date', - y: 'deaths', - z: 'cause', - color: 'cause', - layout: stack({ order: [...causes].reverse() }), - }), - ruleY([0]), - ], - x: { - scale: scaleUtc, - axis: { ticks: { count: 6, format: (value) => month.format(value) } }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { ticks: { count: 5 }, label: 'Deaths' }, - }, - color: { domain: causes, range: causeColors }, - }) -} - -export const mount = tanstackMount( - stackedBarDefinition, - 'Crimean War deaths by cause', -) - -const month = new Intl.DateTimeFormat('en-US', { - month: 'short', - timeZone: 'UTC', -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/bar-vertical-sorted/example.tsx b/benchmarks/conformance/cases/bar-vertical-sorted/example.tsx new file mode 100644 index 00000000..a4c56668 --- /dev/null +++ b/benchmarks/conformance/cases/bar-vertical-sorted/example.tsx @@ -0,0 +1,55 @@ +import { alphabet } from '@charts-poc/demo-data/alphabet' +import { barY, defineChart } from '@tanstack/charts' +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip } from '@tanstack/charts/tooltip' +import { scaleBand, scaleLinear } from 'd3-scale' + +const percent = new Intl.NumberFormat('en-US', { + style: 'percent', + maximumFractionDigits: 1, +}) + +const chart = defineChart(({ width }) => ({ + marks: [ + barY(alphabet, { + x: 'letter', + y: 'frequency', + fill: '#2563eb', + inset: 1, + }), + ], + x: { + scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), + axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, + }, + y: { + scale: scaleLinear, + grid: true, + axis: { + ticks: { + count: 5, + format: (value: number) => percent.format(value), + }, + label: 'Frequency', + }, + }, +})) + +export const exampleAriaLabel = 'Sorted vertical bars' + +export const definition = defineChart(chart, { + keyboard: true, + tooltip: { + use: tooltip, + format: ({ datum }) => + `${datum.letter} · ${percent.format(datum.frequency)} frequency`, + }, +}) + +export const createExampleChart = () => definition + +export default function AlphabetFrequencyChart() { + return ( + + ) +} diff --git a/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts b/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts index 89b5cd39..352e831c 100644 --- a/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts +++ b/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts @@ -1,44 +1,11 @@ -import { alphabet } from '@charts-poc/demo-data/alphabet' -import { barY, defineChart } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const percent = new Intl.NumberFormat('en-US', { - style: 'percent', - maximumFractionDigits: 1, -}) +export * from './example' -const definition = (_input: ConformanceInput) => - defineChart(({ width }) => { - return { - marks: [ - barY(alphabet, { - x: 'letter', - y: 'frequency', - fill: '#2563eb', - inset: 1, - }), - ], - x: { - scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, - }, - y: { - scale: scaleLinear, - grid: true, - axis: { - ticks: { - count: 5, - format: (value: number) => percent.format(value), - }, - label: 'Frequency', - }, - }, - } - }) +export const mount = tanstackExampleMount( + () => createExampleChart(), + exampleAriaLabel, +) -export const mount = tanstackMount(definition, 'Sorted vertical bars', { - format: ({ datum }) => - `${datum.letter} · ${percent.format(datum.frequency)} frequency`, -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/facets-anscombe/example.tsx b/benchmarks/conformance/cases/facets-anscombe/example.tsx new file mode 100644 index 00000000..633819af --- /dev/null +++ b/benchmarks/conformance/cases/facets-anscombe/example.tsx @@ -0,0 +1,64 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { anscombe } from '@charts-poc/demo-data/anscombe' +import { defineChart, dot, facet } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' + +export const definition = (input: ExampleOptions) => + defineChart({ + marks: [ + facet(anscombe, { + by: 'series', + columns: 4, + gap: input.preview === true ? 4 : 12, + label: input.preview === true ? false : (series) => String(series), + chart: (facetRows) => ({ + marks: [ + dot(facetRows, { + x: 'x', + y: 'y', + r: 3.5, + fill: '#2563eb', + }), + ], + x: { + scale: scaleLinear().domain([3, 20]), + grid: input.preview !== true, + axis: input.preview === true ? false : { ticks: { count: 5 } }, + }, + y: { + scale: scaleLinear().domain([2, 14]), + grid: input.preview !== true, + axis: input.preview === true ? false : { ticks: { count: 4 } }, + }, + }), + }), + ], + margin: 0, + }) +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = "Anscombe's quartet small multiples" + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/facets-anscombe/tanstack.ts b/benchmarks/conformance/cases/facets-anscombe/tanstack.ts index 2476f4a2..6c029e65 100644 --- a/benchmarks/conformance/cases/facets-anscombe/tanstack.ts +++ b/benchmarks/conformance/cases/facets-anscombe/tanstack.ts @@ -1,44 +1,8 @@ -import { anscombe } from '@charts-poc/demo-data/anscombe' -import { defineChart, dot, facet } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const definition = (input: ConformanceInput) => - defineChart({ - marks: [ - facet(anscombe, { - by: 'series', - columns: 4, - gap: input.preview === true ? 4 : 12, - label: input.preview === true ? false : (series) => String(series), - chart: (facetRows) => ({ - marks: [ - dot(facetRows, { - x: 'x', - y: 'y', - r: 3.5, - fill: '#2563eb', - }), - ], - x: { - scale: scaleLinear().domain([3, 20]), - grid: input.preview !== true, - axis: input.preview === true ? false : { ticks: { count: 5 } }, - }, - y: { - scale: scaleLinear().domain([2, 14]), - grid: input.preview !== true, - axis: input.preview === true ? false : { ticks: { count: 4 } }, - }, - }), - }), - ], - margin: 0, - }) +export * from './example' -export const mount = tanstackMount( - definition, - "Anscombe's quartet small multiples", - true, -) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) + +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/heatmap-labeled/example.tsx b/benchmarks/conformance/cases/heatmap-labeled/example.tsx new file mode 100644 index 00000000..44c4f6d1 --- /dev/null +++ b/benchmarks/conformance/cases/heatmap-labeled/example.tsx @@ -0,0 +1,106 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { simpsons } from '@charts-poc/demo-data/simpsons' +import { cell, colorGradientLegend, defineChart, text } from '@tanstack/charts' +import { scaleBand, scaleLinear } from 'd3-scale' + +const ratingColors = ['#8e0152', '#f7f7f7', '#276419'] +const previewEpisodes = simpsons.filter( + (row) => row.season <= 4 && row.number_in_season <= 7, +) + +export const definition = (input: ExampleOptions) => { + const episodes = input.preview ? previewEpisodes : simpsons + const episodeDomain = [ + ...new Set(episodes.map((row) => row.number_in_season)), + ].sort((left, right) => left - right) + const seasonDomain = [...new Set(episodes.map((row) => row.season))].sort( + (left, right) => left - right, + ) + const ratedEpisodes = episodes.filter((row) => row.imdb_rating !== null) + const unratedEpisodes = episodes.filter((row) => row.imdb_rating === null) + + return defineChart({ + marks: [ + cell(ratedEpisodes, { + x: 'number_in_season', + y: 'season', + color: 'imdb_rating', + inset: 1, + }), + cell(unratedEpisodes, { + x: 'number_in_season', + y: 'season', + fill: '#d1d5db', + inset: 1, + }), + text(episodes, { + x: 'number_in_season', + y: 'season', + text: (row) => + row.imdb_rating === null ? '–' : row.imdb_rating.toFixed(1), + fill: (row) => + row.imdb_rating !== null && + (row.imdb_rating < 5.5 || row.imdb_rating > 8.6) + ? '#f8fafc' + : '#0f172a', + fontSize: 10, + fontWeight: 600, + }), + ], + x: { + scale: scaleBand() + .domain(episodeDomain) + .paddingInner(0.04) + .paddingOuter(0.02), + axis: { label: 'Episode' }, + }, + y: { + scale: scaleBand() + .domain(seasonDomain) + .paddingInner(0.04) + .paddingOuter(0.02), + axis: { label: 'Season' }, + }, + color: { + scale: () => scaleLinear().range(ratingColors), + legend: colorGradientLegend({ label: 'IMDb rating', steps: 6 }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'The Simpsons episode ratings' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.title} · Season ${datum.season}, episode ${datum.number_in_season} · ${ + datum.imdb_rating === null + ? 'No IMDb rating' + : `${datum.imdb_rating.toFixed(1)} IMDb` + }`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts b/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts index b7815554..6c029e65 100644 --- a/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts +++ b/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts @@ -1,79 +1,8 @@ -import { simpsons } from '@charts-poc/demo-data/simpsons' -import { cell, colorGradientLegend, defineChart, text } from '@tanstack/charts' -import { scaleBand, scaleLinear } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const ratingColors = ['#8e0152', '#f7f7f7', '#276419'] -const previewEpisodes = simpsons.filter( - (row) => row.season <= 4 && row.number_in_season <= 7, -) +export * from './example' -const definition = (input: ConformanceInput) => { - const episodes = input.preview ? previewEpisodes : simpsons - const episodeDomain = [ - ...new Set(episodes.map((row) => row.number_in_season)), - ].sort((left, right) => left - right) - const seasonDomain = [...new Set(episodes.map((row) => row.season))].sort( - (left, right) => left - right, - ) - const ratedEpisodes = episodes.filter((row) => row.imdb_rating !== null) - const unratedEpisodes = episodes.filter((row) => row.imdb_rating === null) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - cell(ratedEpisodes, { - x: 'number_in_season', - y: 'season', - color: 'imdb_rating', - inset: 1, - }), - cell(unratedEpisodes, { - x: 'number_in_season', - y: 'season', - fill: '#d1d5db', - inset: 1, - }), - text(episodes, { - x: 'number_in_season', - y: 'season', - text: (row) => - row.imdb_rating === null ? '–' : row.imdb_rating.toFixed(1), - fill: (row) => - row.imdb_rating !== null && - (row.imdb_rating < 5.5 || row.imdb_rating > 8.6) - ? '#f8fafc' - : '#0f172a', - fontSize: 10, - fontWeight: 600, - }), - ], - x: { - scale: scaleBand() - .domain(episodeDomain) - .paddingInner(0.04) - .paddingOuter(0.02), - axis: { label: 'Episode' }, - }, - y: { - scale: scaleBand() - .domain(seasonDomain) - .paddingInner(0.04) - .paddingOuter(0.02), - axis: { label: 'Season' }, - }, - color: { - scale: () => scaleLinear().range(ratingColors), - legend: colorGradientLegend({ label: 'IMDb rating', steps: 6 }), - }, - }) -} - -export const mount = tanstackMount(definition, 'The Simpsons episode ratings', { - format: ({ datum }) => - `${datum.title} · Season ${datum.season}, episode ${datum.number_in_season} · ${ - datum.imdb_rating === null - ? 'No IMDb rating' - : `${datum.imdb_rating.toFixed(1)} IMDb` - }`, -}) +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/histogram/example.tsx b/benchmarks/conformance/cases/histogram/example.tsx new file mode 100644 index 00000000..3ab642a7 --- /dev/null +++ b/benchmarks/conformance/cases/histogram/example.tsx @@ -0,0 +1,73 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { cars } from '@charts-poc/demo-data/cars' +import { binX, defineChart, rect } from '@tanstack/charts' +import { scaleLinear } from 'd3-scale' +import type { CarsRow } from '@charts-poc/demo-data/cars' + +type CarWithEconomy = CarsRow & { 'economy (mpg)': number } + +const completeCars = cars.filter( + (row): row is CarWithEconomy => row['economy (mpg)'] !== null, +) +const boundaries = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50] +export const definition = (input: ExampleOptions) => { + const bins = binX(completeCars.slice(input.revision * 8), { + value: 'economy (mpg)', + thresholds: boundaries, + outputs: { count: { reduce: 'count' } }, + }) + + return defineChart({ + marks: [ + rect(bins, { + x1: 'x1', + x2: 'x2', + y1: () => 0, + y2: 'count', + fill: '#2563eb', + inset: 1, + }), + ], + x: { + scale: scaleLinear, + grid: true, + axis: { label: 'Fuel economy (mpg)' }, + }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Count' } }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Histogram of fuel economy' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: { + use: exampleTooltip, + ...{ + format: ({ datum }) => + `${datum.x1.toLocaleString('en-US')}–${datum.x2.toLocaleString( + 'en-US', + )} · ${datum.count.toLocaleString('en-US')} observations`, + }, + }, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/histogram/tanstack.ts b/benchmarks/conformance/cases/histogram/tanstack.ts index e9c8c711..6c029e65 100644 --- a/benchmarks/conformance/cases/histogram/tanstack.ts +++ b/benchmarks/conformance/cases/histogram/tanstack.ts @@ -1,52 +1,8 @@ -import { cars } from '@charts-poc/demo-data/cars' -import { binX, defineChart, rect } from '@tanstack/charts' -import { scaleLinear } from 'd3-scale' -import { tanstackCase } from '../../shared/mount' -import type { CarsRow } from '@charts-poc/demo-data/cars' -import type { ConformanceInput } from '../../types' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -type CarWithEconomy = CarsRow & { 'economy (mpg)': number } +export * from './example' -const completeCars = cars.filter( - (row): row is CarWithEconomy => row['economy (mpg)'] !== null, -) -const boundaries = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50] -const definition = (input: ConformanceInput) => { - const bins = binX(completeCars.slice(input.revision * 8), { - value: 'economy (mpg)', - thresholds: boundaries, - outputs: { count: { reduce: 'count' } }, - }) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - rect(bins, { - x1: 'x1', - x2: 'x2', - y1: () => 0, - y2: 'count', - fill: '#2563eb', - inset: 1, - }), - ], - x: { - scale: scaleLinear, - grid: true, - axis: { label: 'Fuel economy (mpg)' }, - }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Count' } }, - }) -} - -export const catalogCase = tanstackCase( - definition, - 'Histogram of fuel economy', - { - format: ({ datum }) => - `${datum.x1.toLocaleString('en-US')}–${datum.x2.toLocaleString( - 'en-US', - )} · ${datum.count.toLocaleString('en-US')} observations`, - }, -) - -export const mount = catalogCase.mount +export const catalogCase = mount diff --git a/benchmarks/conformance/cases/scatter-bubble/example.tsx b/benchmarks/conformance/cases/scatter-bubble/example.tsx new file mode 100644 index 00000000..71e8ad7d --- /dev/null +++ b/benchmarks/conformance/cases/scatter-bubble/example.tsx @@ -0,0 +1,63 @@ +import { Chart } from '@tanstack/charts/react/tooltip' +import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip' + +import { colorLegend, defineChart, dot } from '@tanstack/charts' +import { scaleLinear, scaleSqrt } from 'd3-scale' +import { bubbleRows } from './model' + +const groupRange = ['#2563eb', '#f97316', '#10b981'] + +export const definition = (input: ExampleOptions) => { + const rows = bubbleRows(input.revision) + + return defineChart({ + marks: [ + dot(rows, { + key: (row) => + `${row.species}:${row.island}:${row.culmen_length_mm}:${row.culmen_depth_mm}:${row.flipper_length_mm}:${row.body_mass_g}:${row.sex}`, + x: 'culmen_length_mm', + y: 'culmen_depth_mm', + color: 'species', + r: 'body_mass_g', + rScale: { + scale: () => scaleSqrt().range([3, 11]), + }, + fillOpacity: 0.78, + stroke: 'currentColor', + strokeOpacity: 0.28, + strokeWidth: 0.75, + }), + ], + x: { scale: scaleLinear, grid: true, axis: { label: 'Bill length (mm)' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Bill depth (mm)' } }, + color: { + range: groupRange, + legend: colorLegend({ label: 'Species' }), + }, + }) +} +export interface ExampleOptions { + width: number + height: number + revision: number + preview?: boolean +} + +export const exampleAriaLabel = 'Bubble scatterplot' + +export const createExampleChart = (options: ExampleOptions) => + defineChart(definition(options), { + keyboard: true, + tooltip: exampleTooltip, + }) + +export const chart = createExampleChart({ + width: 640, + height: 480, + revision: 0, + preview: false, +}) + +export default function Example() { + return +} diff --git a/benchmarks/conformance/cases/scatter-bubble/tanstack.ts b/benchmarks/conformance/cases/scatter-bubble/tanstack.ts index ec7058f7..6c029e65 100644 --- a/benchmarks/conformance/cases/scatter-bubble/tanstack.ts +++ b/benchmarks/conformance/cases/scatter-bubble/tanstack.ts @@ -1,44 +1,8 @@ -import { colorLegend, defineChart, dot } from '@tanstack/charts' -import { scaleLinear, scaleSqrt } from 'd3-scale' -import { tanstackMount } from '../../shared/mount' -import { samplePreviewData } from '../../shared/preview' -import type { ConformanceInput } from '../../types' -import { bubbleRows } from './model' +import { createExampleChart, exampleAriaLabel } from './example' +import { tanstackExampleMount } from '../../shared/mount' -const groupRange = ['#2563eb', '#f97316', '#10b981'] +export * from './example' -const definition = (input: ConformanceInput) => { - const rows = samplePreviewData(bubbleRows(input.revision), input, 80, [ - (row) => row.culmen_length_mm, - (row) => row.culmen_depth_mm, - (row) => row.body_mass_g, - ]) +export const mount = tanstackExampleMount(createExampleChart, exampleAriaLabel) - return defineChart({ - marks: [ - dot(rows, { - key: (row) => - `${row.species}:${row.island}:${row.culmen_length_mm}:${row.culmen_depth_mm}:${row.flipper_length_mm}:${row.body_mass_g}:${row.sex}`, - x: 'culmen_length_mm', - y: 'culmen_depth_mm', - color: 'species', - r: 'body_mass_g', - rScale: { - scale: () => scaleSqrt().range([3, 11]), - }, - fillOpacity: 0.78, - stroke: 'currentColor', - strokeOpacity: 0.28, - strokeWidth: 0.75, - }), - ], - x: { scale: scaleLinear, grid: true, axis: { label: 'Bill length (mm)' } }, - y: { scale: scaleLinear, grid: true, axis: { label: 'Bill depth (mm)' } }, - color: { - range: groupRange, - legend: colorLegend({ label: 'Species' }), - }, - }) -} - -export const mount = tanstackMount(definition, 'Bubble scatterplot') +export const catalogCase = mount diff --git a/benchmarks/conformance/catalog-index.json b/benchmarks/conformance/catalog-index.json index b5a4071b..1360a775 100644 --- a/benchmarks/conformance/catalog-index.json +++ b/benchmarks/conformance/catalog-index.json @@ -1,5 +1,5 @@ { - "schemaVersion": 1, + "schemaVersion": 2, "source": { "repo": "tanstack/charts", "pathRoot": "benchmarks/conformance/" @@ -35,11 +35,7 @@ "maintain": "Change which calendar months are treated as missing while keeping the AAPL rows untouched, the accessor explicit, and compact and wide layouts readable." }, "entries": { - "tanstack": "benchmarks/conformance/cases/01-line-gaps/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/01-line-gaps/plot.ts" - } + "example": "benchmarks/conformance/cases/01-line-gaps/example.tsx" } }, { @@ -77,11 +73,7 @@ "maintain": "Move the direct labels from the newest dated observation to the oldest dated observation of each industry while retaining industry colors, inferred callback data, and readable label space." }, "entries": { - "tanstack": "benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/02-multi-line-end-labels/plot.ts" - } + "example": "benchmarks/conformance/cases/02-multi-line-end-labels/example.tsx" } }, { @@ -119,11 +111,7 @@ "maintain": "Add a 32-degree freezing reference rule and make the band react to light and dark themes while preserving the explicit low/high interval, boundary line geometry, and cast-free field inference." }, "entries": { - "tanstack": "benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/03-temperature-range-band/plot.ts" - } + "example": "benchmarks/conformance/cases/03-temperature-range-band/example.tsx" } }, { @@ -160,11 +148,7 @@ "maintain": "Convert the chart to a normalized 100% stack while keeping both implementations on the same imported industry observations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/04-stacked-time-area/plot.ts" - } + "example": "benchmarks/conformance/cases/04-stacked-time-area/example.tsx" } }, { @@ -196,11 +180,7 @@ "maintain": "Change bar presentation while preserving the source letter and frequency channels, descending order, zero baseline, and visible labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/bar-vertical-sorted/plot.ts" - } + "example": "benchmarks/conformance/cases/bar-vertical-sorted/example.tsx" } }, { @@ -236,11 +216,7 @@ "maintain": "Change the selected metros or container width while keeping the population rank correct and every source label visible." }, "entries": { - "tanstack": "benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/bar-horizontal-ranking/plot.ts" - } + "example": "benchmarks/conformance/cases/bar-horizontal-ranking/example.tsx" } }, { @@ -284,11 +260,7 @@ "maintain": "Change the selected observations or width without stacking or overlapping the two sex groups within each species." }, "entries": { - "tanstack": "benchmarks/conformance/cases/bar-grouped/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/bar-grouped/plot.ts" - } + "example": "benchmarks/conformance/cases/bar-grouped/example.tsx" } }, { @@ -330,11 +302,7 @@ "maintain": "Change the visible date range while keeping cause order, monthly boundaries, inferred colors, and the zero baseline correct." }, "entries": { - "tanstack": "benchmarks/conformance/cases/bar-stacked/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/bar-stacked/plot.ts" - } + "example": "benchmarks/conformance/cases/bar-stacked/example.tsx" } }, { @@ -366,11 +334,7 @@ "maintain": "Change the species palette and radius range without renaming or reshaping the imported observations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/scatter-bubble/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/scatter-bubble/plot.ts" - } + "example": "benchmarks/conformance/cases/scatter-bubble/example.tsx" } }, { @@ -402,11 +366,7 @@ "maintain": "Change the threshold boundaries while retaining the imported rows and keeping the Plot and TanStack transforms semantically equivalent." }, "entries": { - "tanstack": "benchmarks/conformance/cases/histogram/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/histogram/plot.ts" - } + "example": "benchmarks/conformance/cases/histogram/example.tsx" } }, { @@ -443,11 +403,7 @@ "maintain": "Change the color treatment or label contrast while preserving all 600 imported episodes, source fields, cells, and labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/heatmap-labeled/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/heatmap-labeled/plot.ts" - } + "example": "benchmarks/conformance/cases/heatmap-labeled/example.tsx" } }, { @@ -487,11 +443,7 @@ "maintain": "Change the facet layout while preserving shared domains, series order, and all 44 source observations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/facets-anscombe/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/facets-anscombe/plot.ts" - } + "example": "benchmarks/conformance/cases/facets-anscombe/example.tsx" } }, { @@ -524,11 +476,7 @@ "maintain": "Change the selected trading window without changing the source data model, clipping date labels, or introducing casts." }, "entries": { - "tanstack": "benchmarks/conformance/cases/13-interval-timeline/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/13-interval-timeline/plot.ts" - } + "example": "benchmarks/conformance/cases/13-interval-timeline/example.tsx" } }, { @@ -575,11 +523,7 @@ "maintain": "Change the authored uncertainty estimator while retaining contributing-observation lineage, typed reducer outputs, and automatic species spacing." }, "entries": { - "tanstack": "benchmarks/conformance/cases/14-error-bars/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/14-error-bars/plot.ts" - } + "example": "benchmarks/conformance/cases/14-error-bars/example.tsx" } }, { @@ -629,11 +573,7 @@ "maintain": "Switch to boxX while preserving raw-row lineage, Tukey whisker semantics, outliers, stable Run identity, and inferred category types." }, "entries": { - "tanstack": "benchmarks/conformance/cases/15-boxplot/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/15-boxplot/plot.ts" - } + "example": "benchmarks/conformance/cases/15-boxplot/example.tsx" } }, { @@ -674,11 +614,7 @@ "maintain": "Switch between horizontal and vertical orientation while retaining the source letter and frequency fields, sorted order, and zero baseline." }, "entries": { - "tanstack": "benchmarks/conformance/cases/16-lollipop/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/16-lollipop/plot.ts" - } + "example": "benchmarks/conformance/cases/16-lollipop/example.tsx" } }, { @@ -714,11 +650,7 @@ "maintain": "Change the metro selection while preserving the imported wide row model, chronological endpoint colors, and source field names." }, "entries": { - "tanstack": "benchmarks/conformance/cases/17-dumbbell/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/17-dumbbell/plot.ts" - } + "example": "benchmarks/conformance/cases/17-dumbbell/example.tsx" } }, { @@ -751,11 +683,7 @@ "maintain": "Convert the cumulative count to cumulative proportion without reshaping the imported observations or replacing data-driven bin inference with authored boundaries." }, "entries": { - "tanstack": "benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/18-cumulative-histogram/plot.ts" - } + "example": "benchmarks/conformance/cases/18-cumulative-histogram/example.tsx" } }, { @@ -793,11 +721,7 @@ "maintain": "Change the rolling window and alignment while preserving separate high and low calculations and the same imported observations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/19-moving-average-line/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/19-moving-average-line/plot.ts" - } + "example": "benchmarks/conformance/cases/19-moving-average-line/example.tsx" } }, { @@ -834,11 +758,7 @@ "maintain": "Change the industry order while keeping each month normalized to 100%, colors stable, the percent guide accurate, and the imported rows untouched." }, "entries": { - "tanstack": "benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/20-normalized-stacked-area/plot.ts" - } + "example": "benchmarks/conformance/cases/20-normalized-stacked-area/example.tsx" } }, { @@ -872,11 +792,7 @@ "maintain": "Change the stack order or offset while retaining raw industry rows, source identity, equivalent zero-origin wiggle semantics, and inferred interval points." }, "entries": { - "tanstack": "benchmarks/conformance/cases/21-streamgraph/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/21-streamgraph/plot.ts" - } + "example": "benchmarks/conformance/cases/21-streamgraph/example.tsx" } }, { @@ -915,11 +831,7 @@ "maintain": "Change the authored window or deviation multiplier while preserving direct source lineage, full trailing-window semantics, and one shared transformed row set for the area and mean line." }, "entries": { - "tanstack": "benchmarks/conformance/cases/22-bollinger-band/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/22-bollinger-band/plot.ts" - } + "example": "benchmarks/conformance/cases/22-bollinger-band/example.tsx" } }, { @@ -952,11 +864,7 @@ "maintain": "Change the bin interval while preserving equivalent Plot and granular d3-array boundaries, continuous color semantics, and inferred endpoint types." }, "entries": { - "tanstack": "benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/24-quantitative-binned-heatmap/plot.ts" - } + "example": "benchmarks/conformance/cases/24-quantitative-binned-heatmap/example.tsx" } }, { @@ -989,11 +897,7 @@ "maintain": "Change the selected weather window while preserving UTC week alignment, raw date and precipitation fields, complete daily cells, and responsive labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/25-calendar-heatmap/plot.ts" - } + "example": "benchmarks/conformance/cases/25-calendar-heatmap/example.tsx" } }, { @@ -1026,11 +930,7 @@ "maintain": "Reorder response categories while keeping the neutral anchor, response-count lineage, stable colors, and categorical keys explicit in the chart definition." }, "entries": { - "tanstack": "benchmarks/conformance/cases/26-diverging-likert/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/26-diverging-likert/plot.ts" - } + "example": "benchmarks/conformance/cases/26-diverging-likert/example.tsx" } }, { @@ -1070,11 +970,7 @@ "maintain": "Add or reorder metrics while preserving authored metric direction, last-row country selection, normalized quantitative domains, and stable country-event identity." }, "entries": { - "tanstack": "benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/27-parallel-coordinates/plot.ts" - } + "example": "benchmarks/conformance/cases/27-parallel-coordinates/example.tsx" } }, { @@ -1111,11 +1007,7 @@ "maintain": "Change the selected trading window while preserving all 60 interval segments, raw AAPL field names, date identity, and typed endpoint channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/28-candlestick/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/28-candlestick/plot.ts" - } + "example": "benchmarks/conformance/cases/28-candlestick/example.tsx" } }, { @@ -1148,11 +1040,7 @@ "maintain": "Change the year window while keeping adjacent difference semantics, cumulative interval ownership, stable year identity, direct lineage, and the zero-based net total explicit." }, "entries": { - "tanstack": "benchmarks/conformance/cases/29-waterfall/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/29-waterfall/plot.ts" - } + "example": "benchmarks/conformance/cases/29-waterfall/example.tsx" } }, { @@ -1195,11 +1083,7 @@ "maintain": "Change the periods while retaining explicit field-to-year display mapping, nyt_display colors, stable metro-period identity, and the visible fold and selection transforms." }, "entries": { - "tanstack": "benchmarks/conformance/cases/30-slopegraph/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/30-slopegraph/plot.ts" - } + "example": "benchmarks/conformance/cases/30-slopegraph/example.tsx" } }, { @@ -1239,11 +1123,7 @@ "maintain": "Change the selected automobiles while retaining raw source rows, source field names, aggregate regression lineage, and cast-free channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/31-linear-regression/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/31-linear-regression/plot.ts" - } + "example": "benchmarks/conformance/cases/31-linear-regression/example.tsx" } }, { @@ -1276,11 +1156,7 @@ "maintain": "Change the metro selection or arrowhead size while retaining the source endpoint fields, one semantic arrow per metro, and stable identity." }, "entries": { - "tanstack": "benchmarks/conformance/cases/32-change-arrows/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/32-change-arrows/plot.ts" - } + "example": "benchmarks/conformance/cases/32-change-arrows/example.tsx" } }, { @@ -1317,11 +1193,7 @@ "maintain": "Change the selected date window or rolling period without losing exact crossing boundaries, semantic colors, the original Date and Close fields, inferred extents, or cast-free channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/33-difference-chart/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/33-difference-chart/plot.ts" - } + "example": "benchmarks/conformance/cases/33-difference-chart/example.tsx" } }, { @@ -1405,11 +1277,7 @@ "maintain": "Make the tooltip sticky on selection while keeping the raw stock observations, pointer, keyboard, and update behavior aligned." }, "entries": { - "tanstack": "benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/34-pointer-tooltip/plot.ts" - } + "example": "benchmarks/conformance/cases/34-pointer-tooltip/example.tsx" } }, { @@ -1498,11 +1366,7 @@ "maintain": "Add another industry while preserving one swatch and value per group, keyboard navigation by month, and top-layer responsive tooltip positioning." }, "entries": { - "tanstack": "benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/35-grouped-tooltip/plot.ts" - } + "example": "benchmarks/conformance/cases/35-grouped-tooltip/example.tsx" } }, { @@ -1544,11 +1408,7 @@ "maintain": "Change the selected Flare rows while preserving raw-row lineage, stable path and link identities, deterministic static coordinates, and mirrored branch and leaf labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/36-hierarchy-tree/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/36-hierarchy-tree/plot.ts" - } + "example": "benchmarks/conformance/cases/36-hierarchy-tree/example.tsx" } }, { @@ -1585,11 +1445,7 @@ "maintain": "Change the selected cars while preserving final-screen topology, both endpoint records, stable point-derived edge keys, and cast-free source channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/37-delaunay-network/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/37-delaunay-network/plot.ts" - } + "example": "benchmarks/conformance/cases/37-delaunay-network/example.tsx" } }, { @@ -1622,11 +1478,7 @@ "maintain": "Change the selected longitude window while keeping the threshold sequence, source grid dimensions, vertical orientation, and literal contour palette aligned across both implementations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/38-contour-topography/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/38-contour-topography/plot.ts" - } + "example": "benchmarks/conformance/cases/38-contour-topography/example.tsx" } }, { @@ -1658,11 +1510,7 @@ "maintain": "Preserve D3-native density thresholds, pixel bandwidth, scale domains, structured multipolygon output, and responsive final-screen estimation." }, "entries": { - "tanstack": "benchmarks/conformance/cases/39-density-contours/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/39-density-contours/plot.ts" - } + "example": "benchmarks/conformance/cases/39-density-contours/example.tsx" } }, { @@ -1696,11 +1544,7 @@ "maintain": "Preserve source feature ids and properties, responsive planar fitting, one path per published feature, and the geo entrypoint boundary." }, "entries": { - "tanstack": "benchmarks/conformance/cases/40-geojson-map/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/40-geojson-map/plot.ts" - } + "example": "benchmarks/conformance/cases/40-geojson-map/example.tsx" } }, { @@ -1743,11 +1587,7 @@ "maintain": "Change force strengths or the selected subgraph while preserving raw id, group, source, target, and value fields, stable identities, deterministic static layout, and padded quantitative domains." }, "entries": { - "tanstack": "benchmarks/conformance/cases/40-force-directed-network/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/40-force-directed-network/plot.ts" - } + "example": "benchmarks/conformance/cases/40-force-directed-network/example.tsx" } }, { @@ -1779,11 +1619,7 @@ "maintain": "Change the unit precision while preserving the source letter and frequency fields, cumulative rounding, one stable position per unit, responsive near-square cells, and consistent letter colors." }, "entries": { - "tanstack": "benchmarks/conformance/cases/41-waffle-unit-chart/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/41-waffle-unit-chart/plot.ts" - } + "example": "benchmarks/conformance/cases/41-waffle-unit-chart/example.tsx" } }, { @@ -1816,11 +1652,7 @@ "maintain": "Change the field density and vector anchor while retaining typed channels, responsive scales, and one semantic vector per sample." }, "entries": { - "tanstack": "benchmarks/conformance/cases/42-vector-field/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/42-vector-field/plot.ts" - } + "example": "benchmarks/conformance/cases/42-vector-field/example.tsx" } }, { @@ -1852,11 +1684,7 @@ "maintain": "Change bin width and thresholds while preserving raw-row lineage, fixed-pixel bins, stable keys, and cast-free channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/43-hexbin-density/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/43-hexbin-density/plot.ts" - } + "example": "benchmarks/conformance/cases/43-hexbin-density/example.tsx" } }, { @@ -1893,11 +1721,7 @@ "maintain": "Change frame inset and styling while retaining the imported rows, source fields, responsive inner-bound alignment, and suppressed guides." }, "entries": { - "tanstack": "benchmarks/conformance/cases/44-framed-scatter/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/44-framed-scatter/plot.ts" - } + "example": "benchmarks/conformance/cases/44-framed-scatter/example.tsx" } }, { @@ -1930,11 +1754,7 @@ "maintain": "Split the distribution by cylinders while computing ranks within each group and retaining the imported rows and explicit zero-to-one probability domains." }, "entries": { - "tanstack": "benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/50-empirical-cdf/plot.ts" - } + "example": "benchmarks/conformance/cases/50-empirical-cdf/example.tsx" } }, { @@ -1968,11 +1788,7 @@ "maintain": "Change the fixed bin boundaries while preserving explicit species order, facet-local sum normalization, aggregate source lineage, and shared outer scale semantics." }, "entries": { - "tanstack": "benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/51-faceted-distributions/plot.ts" - } + "example": "benchmarks/conformance/cases/51-faceted-distributions/example.tsx" } }, { @@ -2005,11 +1821,7 @@ "maintain": "Change the dot radius while retaining the source field and updating both collision distance and mark radius for deterministic responsive layout." }, "entries": { - "tanstack": "benchmarks/conformance/cases/52-beeswarm-dodge/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/52-beeswarm-dodge/plot.ts" - } + "example": "benchmarks/conformance/cases/52-beeswarm-dodge/example.tsx" } }, { @@ -2042,11 +1854,7 @@ "maintain": "Change the selected Flare rows, log base, or domain while rejecting non-positive sizes and preserving the linear hierarchy-depth encoding." }, "entries": { - "tanstack": "benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/53-log-scale-scatter/plot.ts" - } + "example": "benchmarks/conformance/cases/53-log-scale-scatter/example.tsx" } }, { @@ -2087,11 +1895,7 @@ "maintain": "Change the industry or year selection while retaining the original date, industry, and unemployed fields through the ranking transform, stable series colors, and labels at the newest observation." }, "entries": { - "tanstack": "benchmarks/conformance/cases/54-bump-ranking/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/54-bump-ranking/plot.ts" - } + "example": "benchmarks/conformance/cases/54-bump-ranking/example.tsx" } }, { @@ -2132,11 +1936,7 @@ "maintain": "Change the industry selection or index date while retaining the original date, industry, and unemployed fields through normalization, stable colors, inferred channels, and percentage labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/55-indexed-multi-line/plot.ts" - } + "example": "benchmarks/conformance/cases/55-indexed-multi-line/example.tsx" } }, { @@ -2181,11 +1981,7 @@ "maintain": "Change label cadence and arrow sampling while retaining chronological order, responsive quantitative scales, and a single inferred row type." }, "entries": { - "tanstack": "benchmarks/conformance/cases/56-connected-scatter/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/56-connected-scatter/plot.ts" - } + "example": "benchmarks/conformance/cases/56-connected-scatter/example.tsx" } }, { @@ -2223,11 +2019,7 @@ "maintain": "Change the bin boundaries while retaining complete boundary semantics, responsive shared scales, inferred fields, and separate scatter and marginal regions." }, "entries": { - "tanstack": "benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/57-scatter-marginal-histograms/plot.ts" - } + "example": "benchmarks/conformance/cases/57-scatter-marginal-histograms/example.tsx" } }, { @@ -2268,11 +2060,7 @@ "maintain": "Change the selected date window while preserving the original Date and Close fields, unique extrema selection, stable point keys, inferred extents, and labels that stay inside the plot." }, "entries": { - "tanstack": "benchmarks/conformance/cases/58-select-extrema/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/58-select-extrema/plot.ts" - } + "example": "benchmarks/conformance/cases/58-select-extrema/example.tsx" } }, { @@ -2310,11 +2098,7 @@ "maintain": "Add another reducer or change the species order while preserving the original source fields, source category order, zero baseline, and inferred aggregate rows." }, "entries": { - "tanstack": "benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/59-grouped-reducer-bars/plot.ts" - } + "example": "benchmarks/conformance/cases/59-grouped-reducer-bars/example.tsx" } }, { @@ -2352,11 +2136,7 @@ "maintain": "Change the date window or lag interval while preserving the original Date and Close fields through the pair transform, input order, incomplete-pair removal, stable keys, explicit equal domains, and the identity reference." }, "entries": { - "tanstack": "benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/60-lag-autocorrelation/plot.ts" - } + "example": "benchmarks/conformance/cases/60-lag-autocorrelation/example.tsx" } }, { @@ -2395,11 +2175,7 @@ "maintain": "Change the percentile interval while preserving the original date, industry, and unemployed fields through the reducer, UTC grouping, inferred extents, stable keys, and matching D3 quantile semantics." }, "entries": { - "tanstack": "benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/61-quantile-ribbon/plot.ts" - } + "example": "benchmarks/conformance/cases/61-quantile-ribbon/example.tsx" } }, { @@ -2440,11 +2216,7 @@ "maintain": "Change the selected seasons while keeping semantic category baselines, grouped bin lineage, normalization, labels, identity, colors, and responsive overlap geometry aligned." }, "entries": { - "tanstack": "benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/62-ridgeline-density/plot.ts" - } + "example": "benchmarks/conformance/cases/62-ridgeline-density/example.tsx" } }, { @@ -2491,11 +2263,7 @@ "maintain": "Change the authored body-mass boundaries while preserving bin and summary lineage, semantic species centers, normalized widths, median indicators, colors, and responsive symmetry." }, "entries": { - "tanstack": "benchmarks/conformance/cases/63-violin-distributions/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/63-violin-distributions/plot.ts" - } + "example": "benchmarks/conformance/cases/63-violin-distributions/example.tsx" } }, { @@ -2533,11 +2301,7 @@ "maintain": "Add a question or response while preserving semantic categories, both normalization denominators, explicit Likert ordering, nested source lineage, stable cell identity, labels, colors, and complete zero-to-one bounds." }, "entries": { - "tanstack": "benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/64-marimekko-mosaic/plot.ts" - } + "example": "benchmarks/conformance/cases/64-marimekko-mosaic/example.tsx" } }, { @@ -2612,11 +2376,7 @@ "maintain": "Change the selected cars while retaining bounded structured cells, stable source identity, dot-owned nearest focus, matching color domains, typed tooltip rows, responsive clipping, and keyboard focus." }, "entries": { - "tanstack": "benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/65-voronoi-nearest-tooltip/plot.ts" - } + "example": "benchmarks/conformance/cases/65-voronoi-nearest-tooltip/example.tsx" } }, { @@ -2662,11 +2422,7 @@ "maintain": "Select a different weather window while preserving source dates and measures, mark order, colors, and cast-free channel inference." }, "entries": { - "tanstack": "benchmarks/conformance/cases/70-composed-chart/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/70-composed-chart/recharts.ts" - } + "example": "benchmarks/conformance/cases/70-composed-chart/example.tsx" } }, { @@ -2701,11 +2457,7 @@ "maintain": "Select a different observation window while preserving grouped-count lineage, the signed channel, explicit stack and color order, symmetric domain, species order, and absolute count tick labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/71-recharts-population-pyramid/recharts.ts" - } + "example": "benchmarks/conformance/cases/71-recharts-population-pyramid/example.tsx" } }, { @@ -2738,11 +2490,7 @@ "maintain": "Select a different weather window while preserving source dates and measures, explicit stack intervals, shared group placement, fixed colors, and inferred numeric extent." }, "entries": { - "tanstack": "benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/72-recharts-mixed-bars/recharts.ts" - } + "example": "benchmarks/conformance/cases/72-recharts-mixed-bars/example.tsx" } }, { @@ -2775,11 +2523,7 @@ "maintain": "Change the selected car window while preserving raw field names, stable model identity, inferred extents, cylinder colors, and cast-free channels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/73-many-point-scatter/recharts.ts" - } + "example": "benchmarks/conformance/cases/73-many-point-scatter/example.tsx" } }, { @@ -2819,11 +2563,7 @@ "maintain": "Change the selected Flare subtree while preserving authored order, raw-row lineage, deterministic squarification, stable leaf identity, colors, and fitted label placement." }, "entries": { - "tanstack": "benchmarks/conformance/cases/74-recharts-treemap/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/74-recharts-treemap/recharts.ts" - } + "example": "benchmarks/conformance/cases/74-recharts-treemap/example.tsx" } }, { @@ -2865,11 +2605,7 @@ "maintain": "Change the subject or events while preserving authored metric direction, source population, event order, percentage guide values, stable event identity, and label placement." }, "entries": { - "tanstack": "benchmarks/conformance/cases/75-radar/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/75-radar/recharts.ts" - } + "example": "benchmarks/conformance/cases/75-radar/example.tsx" } }, { @@ -2906,11 +2642,7 @@ "maintain": "Change the selected letters while preserving raw field names, source order, full-circle orientation, responsive radius, stable identities, and exact categorical fills." }, "entries": { - "tanstack": "benchmarks/conformance/cases/76-pie/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/76-pie/recharts.ts" - } + "example": "benchmarks/conformance/cases/76-pie/example.tsx" } }, { @@ -2947,11 +2679,7 @@ "maintain": "Change the selected letters while preserving raw field names, flat source lineage, the inner-radius ratio, clockwise source order, stable identities, and exact categorical fills." }, "entries": { - "tanstack": "benchmarks/conformance/cases/77-donut/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/77-donut/recharts.ts" - } + "example": "benchmarks/conformance/cases/77-donut/example.tsx" } }, { @@ -2989,11 +2717,7 @@ "maintain": "Change the selected question while preserving the visible reducer, flat source lineage, partial sweep, responsive radii, stable agreement and remainder identities, and exact fills." }, "entries": { - "tanstack": "benchmarks/conformance/cases/78-gauge/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/78-gauge/recharts.ts" - } + "example": "benchmarks/conformance/cases/78-gauge/example.tsx" } }, { @@ -3188,11 +2912,7 @@ "maintain": "Change the selected industries while preserving semantic date anchors, native grouped focus, the definition-owned focus guide and tooltip, SVG accessibility, and the original observations." }, "entries": { - "tanstack": "benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/80-echarts-axis-pointer/echarts.ts" - } + "example": "benchmarks/conformance/cases/80-echarts-axis-pointer/example.tsx" } }, { @@ -3440,11 +3160,7 @@ "maintain": "Keep one grouped line mark over the raw selected rows, stable series IDs, the full categorical and quantitative domains, and application-owned controlled visibility through the native interactive legend." }, "entries": { - "tanstack": "benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/81-recharts-interactive-legend/recharts.ts" - } + "example": "benchmarks/conformance/cases/81-recharts-interactive-legend/example.tsx" } }, { @@ -3740,11 +3456,7 @@ "maintain": "Keep one raw observation dataset, stable semantic keys, controlled selection, and the decorative selected mark in the definition. Retain the semantic HTML table, status announcement, clear action, representative-row policy, and persistence in the application shell." }, "entries": { - "tanstack": "benchmarks/conformance/cases/82-chart-table-selection/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/82-chart-table-selection/recharts.ts" - } + "example": "benchmarks/conformance/cases/82-chart-table-selection/example.tsx" } }, { @@ -4346,11 +4058,7 @@ "maintain": "Keep the selected date and window as application state, preserve them across catalog updates and resizes, and resolve test targets through chart coordinates rather than DOM order." }, "entries": { - "tanstack": "benchmarks/conformance/cases/83-focus-context-window/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/83-focus-context-window/plot.ts" - } + "example": "benchmarks/conformance/cases/83-focus-context-window/example.tsx" } }, { @@ -4721,11 +4429,7 @@ "maintain": "Preserve twelve stable month IDs, two stacked generation bars per month, the gray consumption area and line, the compact two-row summary, always-visible solar-coverage footer, six detailed rows, four-bar nested chart, native inert/status versus interactive/dialog semantics, reduced-motion behavior, and Escape plus close-button dismissal." }, "entries": { - "tanstack": "benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/recharts.ts" - } + "example": "benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/example.tsx" } }, { @@ -5017,11 +4721,7 @@ "maintain": "Keep browser scrolling in the application layer, use the resolved y scale to align the fixed lane rail, retain stable task IDs and explicit UTC and band scales, and preserve scroll position across updates." }, "entries": { - "tanstack": "benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/85-scrollable-resource-lanes/echarts.ts" - } + "example": "benchmarks/conformance/cases/85-scrollable-resource-lanes/example.tsx" } }, { @@ -5351,11 +5051,7 @@ "maintain": "Use each observation date as stable identity, derive the visible slice from the explicit viewport, and advance through source rows independently from the locked scale domain." }, "entries": { - "tanstack": "benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/86-streaming-window-preservation/echarts.ts" - } + "example": "benchmarks/conformance/cases/86-streaming-window-preservation/example.tsx" } }, { @@ -5722,11 +5418,7 @@ "maintain": "Keep source dates as row identity, group points by view, and let one view-grid definition own linked focus guides and pinning while the semantic summary only observes native focus changes." }, "entries": { - "tanstack": "benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/87-echarts-synchronized-cursors/echarts.ts" - } + "example": "benchmarks/conformance/cases/87-echarts-synchronized-cursors/example.tsx" } }, { @@ -5995,11 +5687,7 @@ "maintain": "Keep transient cursor movement DOM-only, constrain it to resolved chart bounds, hide it on leave, and express cursor geometry and scale inversion through the continuous cursor behavior." }, "entries": { - "tanstack": "benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/88-echarts-free-cursor/echarts.ts" - } + "example": "benchmarks/conformance/cases/88-echarts-free-cursor/example.tsx" } }, { @@ -6460,11 +6148,7 @@ "maintain": "Keep the semantic range controlled by the application, let the brush behavior own resolved-scale mapping, snapping, painting, handles, and gesture lifecycle, and keep the range summary outside the renderer." }, "entries": { - "tanstack": "benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/89-brush-range-selection/echarts.ts" - } + "example": "benchmarks/conformance/cases/89-brush-range-selection/example.tsx" } }, { @@ -7168,11 +6852,7 @@ "maintain": "Keep the semantic window controlled by the application, let zoomX own resolved-scale inversion and gesture lifecycle, leave wheel and vertical touch scrolling unconsumed until intentional plot focus, and keep status, reset, visible-row, and persistence policy outside the renderer." }, "entries": { - "tanstack": "benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/90-zoomable-time-window/echarts.ts" - } + "example": "benchmarks/conformance/cases/90-zoomable-time-window/example.tsx" } }, { @@ -7836,11 +7516,7 @@ "maintain": "Keep the playback clock and transport controls in the application, let handleX own final-scale placement, irregular-date snapping, direct manipulation, cancellation, and slider semantics, and preserve the semantic frame across catalog updates and resizes." }, "entries": { - "tanstack": "benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/91-timeline-playback-scrubber/echarts.ts" - } + "example": "benchmarks/conformance/cases/91-timeline-playback-scrubber/example.tsx" } }, { @@ -8405,11 +8081,7 @@ "maintain": "Keep scale-resolved placement, candidate snapping, direct manipulation, cancellation, and slider semantics in handleX; keep event IDs, date constraints, validation, and duration status in application-owned state." }, "entries": { - "tanstack": "benchmarks/conformance/cases/92-editable-event-range/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/92-editable-event-range/echarts.ts" - } + "example": "benchmarks/conformance/cases/92-editable-event-range/example.tsx" } }, { @@ -8457,11 +8129,7 @@ "maintain": "Change the selected letters while preserving raw letter and frequency fields, source order, arc identity, responsive offsets, side-aware text anchors, and label-to-slice color." }, "entries": { - "tanstack": "benchmarks/conformance/cases/93-labeled-pie/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/93-labeled-pie/recharts.ts" - } + "example": "benchmarks/conformance/cases/93-labeled-pie/example.tsx" } }, { @@ -8504,11 +8172,7 @@ "maintain": "Change the selected letters while preserving flat source lineage, semantic letter identities, the responsive inner-radius ratio, and a center total recomputed from the same selected rows." }, "entries": { - "tanstack": "benchmarks/conformance/cases/94-center-donut/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/94-center-donut/recharts.ts" - } + "example": "benchmarks/conformance/cases/94-center-donut/example.tsx" } }, { @@ -8546,11 +8210,7 @@ "maintain": "Change the selected letters while preserving flat source lineage, raw letter and frequency fields, explicit gap size, responsive radii, rounded corners, source order, and semantic arc identity." }, "entries": { - "tanstack": "benchmarks/conformance/cases/95-rounded-donut/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/95-rounded-donut/recharts.ts" - } + "example": "benchmarks/conformance/cases/95-rounded-donut/example.tsx" } }, { @@ -8587,11 +8247,7 @@ "maintain": "Keep domain-specific family aggregation in the case while preserving independent declarative allocation, aligned family totals, ring order, flat lineage, semantic keys, the inter-ring gap, and responsive radii." }, "entries": { - "tanstack": "benchmarks/conformance/cases/96-nested-donut/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/96-nested-donut/recharts.ts" - } + "example": "benchmarks/conformance/cases/96-nested-donut/example.tsx" } }, { @@ -8630,11 +8286,7 @@ "maintain": "Change the selected letters without changing source order, the dataset-wide maximum, the responsive 30%-to-100% radial range, the physical zero-radius baseline, raw row identity, or semantic letter keys and colors." }, "entries": { - "tanstack": "benchmarks/conformance/cases/97-rose/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/97-rose/recharts.ts" - } + "example": "benchmarks/conformance/cases/97-rose/example.tsx" } }, { @@ -8686,11 +8338,7 @@ "maintain": "Change the selected county without changing threshold or tick boundaries; preserve semantic mark keys, flat pie lineage, the zero-to-thirty-percent angle domain, the responsive radius, and native polar mark ownership." }, "entries": { - "tanstack": "benchmarks/conformance/cases/98-needle-gauge/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/98-needle-gauge/recharts.ts" - } + "example": "benchmarks/conformance/cases/98-needle-gauge/example.tsx" } }, { @@ -8735,11 +8383,7 @@ "maintain": "Update either profile while preserving authored metric direction, event order, first-row country selection, stable country-event identity, the fixed zero-to-one radial domain, and shared guide geometry." }, "entries": { - "tanstack": "benchmarks/conformance/cases/99-comparative-radar/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/99-comparative-radar/recharts.ts" - } + "example": "benchmarks/conformance/cases/99-comparative-radar/example.tsx" } }, { @@ -8776,11 +8420,7 @@ "maintain": "Change the selected letters without changing radius-band order or the shared angular domain; preserve raw field names, responsive radius ranges, semantic keys, and exact per-letter fills." }, "entries": { - "tanstack": "benchmarks/conformance/cases/100-radial-bars/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/100-radial-bars/recharts.ts" - } + "example": "benchmarks/conformance/cases/100-radial-bars/example.tsx" } }, { @@ -8817,11 +8457,7 @@ "maintain": "Change the selected Flare rows while preserving raw row lineage, hierarchy order, global angular allocation, responsive rings, stable node identity, and branch color inheritance." }, "entries": { - "tanstack": "benchmarks/conformance/cases/101-sunburst/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/101-sunburst/recharts.ts" - } + "example": "benchmarks/conformance/cases/101-sunburst/example.tsx" } }, { @@ -8857,11 +8493,7 @@ "maintain": "Preserve source field names, the explicit country-name aliases, missing-atlas behavior for tiny states, threshold color semantics, and one shared topology conversion." }, "entries": { - "tanstack": "benchmarks/conformance/cases/102-world-choropleth/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/102-world-choropleth/plot.ts" - } + "example": "benchmarks/conformance/cases/102-world-choropleth/example.tsx" } }, { @@ -8897,11 +8529,7 @@ "maintain": "Keep the join and centroid transform visible, preserve source population values, draw larger symbols first, and use the same square-root radius range in both renderers." }, "entries": { - "tanstack": "benchmarks/conformance/cases/103-bubble-map/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/103-bubble-map/plot.ts" - } + "example": "benchmarks/conformance/cases/103-bubble-map/example.tsx" } }, { @@ -8937,11 +8565,7 @@ "maintain": "Keep the real land topology conversion, sphere, D3 graticule, rotation, clipping, responsive fit, and paint order aligned." }, "entries": { - "tanstack": "benchmarks/conformance/cases/104-orthographic-globe/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/104-orthographic-globe/plot.ts" - } + "example": "benchmarks/conformance/cases/104-orthographic-globe/example.tsx" } }, { @@ -8977,11 +8601,7 @@ "maintain": "Preserve the published coordinate order, visible route construction, ten-degree rotation, antimeridian clipping, D3 geographic resampling, and responsive sphere fitting." }, "entries": { - "tanstack": "benchmarks/conformance/cases/105-route-map/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/105-route-map/plot.ts" - } + "example": "benchmarks/conformance/cases/105-route-map/example.tsx" } }, { @@ -9022,11 +8642,7 @@ "maintain": "Keep source rows and field names intact, preserve chronological order, explicit calendar and temperature domains, responsive center and radius, and visible year selection." }, "entries": { - "tanstack": "benchmarks/conformance/cases/106-polar-line/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/106-polar-line/echarts.ts" - } + "example": "benchmarks/conformance/cases/106-polar-line/example.tsx" } }, { @@ -9066,11 +8682,7 @@ "maintain": "Keep source longitude, latitude, u, and v fields intact; preserve the visible band selection, fixed direction and speed domains, responsive layout, and constant symbol size." }, "entries": { - "tanstack": "benchmarks/conformance/cases/107-polar-scatter/tanstack.ts", - "reference": { - "renderer": "echarts", - "path": "benchmarks/conformance/cases/107-polar-scatter/echarts.ts" - } + "example": "benchmarks/conformance/cases/107-polar-scatter/example.tsx" } }, { @@ -9108,11 +8720,7 @@ "maintain": "Preserve source field names, explicit name aliases, atlas identity, responsive projection fitting, stable country keys, quantized colors, and theme-aware borders." }, "entries": { - "tanstack": "benchmarks/conformance/cases/108-country-choropleth/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/108-country-choropleth/plot.ts" - } + "example": "benchmarks/conformance/cases/108-country-choropleth/example.tsx" } }, { @@ -9150,11 +8758,7 @@ "maintain": "Preserve all source rates and names, visible TopoJSON conversion and FIPS join, the explicit Albers-USA territory filter, stable county identity, Alaska and Hawaii insets, quantile colors, and one path per projected county." }, "entries": { - "tanstack": "benchmarks/conformance/cases/109-us-state-choropleth/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/109-us-state-choropleth/plot.ts" - } + "example": "benchmarks/conformance/cases/109-us-state-choropleth/example.tsx" } }, { @@ -9192,11 +8796,7 @@ "maintain": "Preserve pane order, shared real land geometry, per-projection identity, responsive fitting, sphere outlines, and theme-aware borders while updating paint." }, "entries": { - "tanstack": "benchmarks/conformance/cases/110-projection-gallery/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/110-projection-gallery/plot.ts" - } + "example": "benchmarks/conformance/cases/110-projection-gallery/example.tsx" } }, { @@ -9239,11 +8839,7 @@ "maintain": "Keep this example intentionally minimal: preserve the simple split-and-recombine dataset, a revision-varying split with a total of 10, flow conservation, responsive bounds, flat link caps, one label per node, and native link/rect/text composition without adding semantic color overrides or decorative labels." }, "entries": { - "tanstack": "benchmarks/conformance/cases/111-basic-sankey/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/111-basic-sankey/recharts.ts" - } + "example": "benchmarks/conformance/cases/111-basic-sankey/example.tsx" } }, { @@ -9287,11 +8883,7 @@ "maintain": "Change statement values through the bounded revision model while preserving unique IDs, flow conservation at every intermediate subtotal, left-aligned sink depths, responsive bounds, semantic profit and cost colors, flat link caps, direct labels, and native link/rect/text composition." }, "entries": { - "tanstack": "benchmarks/conformance/cases/111-sankey-flow/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/111-sankey-flow/recharts.ts" - } + "example": "benchmarks/conformance/cases/111-sankey-flow/example.tsx" } }, { @@ -9330,11 +8922,7 @@ "maintain": "Preserve the eight stable datum keys, baseline growth, independent line timing, featured-datum override, reduced-motion behavior, and direct @tanstack/charts/motion renderer boundary." }, "entries": { - "tanstack": "benchmarks/conformance/cases/112-motion-entrance/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/112-motion-entrance/plot.ts" - } + "example": "benchmarks/conformance/cases/112-motion-entrance/example.tsx" } }, { @@ -9374,11 +8962,7 @@ "maintain": "Preserve stable datum keys, enter/update/exit phase semantics, value and velocity continuity during interrupted springs, rest-based settling, presentation-aware pointer resolution, and the direct @tanstack/charts/motion renderer boundary." }, "entries": { - "tanstack": "benchmarks/conformance/cases/113-motion-updates/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/113-motion-updates/plot.ts" - } + "example": "benchmarks/conformance/cases/113-motion-updates/example.tsx" } }, { @@ -9411,11 +8995,7 @@ "maintain": "Keep both series keyed, preserve value and velocity on spring interruption, and let the comparison series use a distinct transition." }, "entries": { - "tanstack": "benchmarks/conformance/cases/114-spring-line-motion/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/114-spring-line-motion/plot.ts" - } + "example": "benchmarks/conformance/cases/114-spring-line-motion/example.tsx" } }, { @@ -9454,11 +9034,7 @@ "maintain": "Keep motion configuration renderer-neutral, preserve spring momentum on interrupted mark updates, and keep guide transitions as local definition overrides." }, "entries": { - "tanstack": "benchmarks/conformance/cases/115-definition-motion/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/115-definition-motion/plot.ts" - } + "example": "benchmarks/conformance/cases/115-definition-motion/example.tsx" } }, { @@ -9495,11 +9071,7 @@ "maintain": "Keep every datum keyed across modes, preserve the fixed path sample count, and verify that an interrupted spring carries path-token velocity into the next chart form." }, "entries": { - "tanstack": "benchmarks/conformance/cases/116-geometry-morph/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/116-geometry-morph/plot.ts" - } + "example": "benchmarks/conformance/cases/116-geometry-morph/example.tsx" } }, { @@ -9636,11 +9208,7 @@ "maintain": "Keep focus semantics in mark states, preserve spring velocity and label alignment during rapid pointer retargeting, and support the same focus movement from the keyboard." }, "entries": { - "tanstack": "benchmarks/conformance/cases/117-focus-cursor-motion/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/117-focus-cursor-motion/plot.ts" - } + "example": "benchmarks/conformance/cases/117-focus-cursor-motion/example.tsx" } }, { @@ -9718,11 +9286,7 @@ "maintain": "Preserve the explicit UTC day domain, 364 complete daily cells, transform lineage, deterministic revisions, Sunday week alignment, twelve month labels, weekday order, accessible color meaning, late-timeline activity shape, and consistent zero-usage treatment." }, "entries": { - "tanstack": "benchmarks/conformance/cases/118-token-usage-calendar/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/118-token-usage-calendar/plot.ts" - } + "example": "benchmarks/conformance/cases/118-token-usage-calendar/example.tsx" } }, { @@ -10001,11 +9565,7 @@ "maintain": "Keep the labeled x band and y rule as stable renderer-native guides, preserve their spring motion and keyed update continuity, and verify the band's exact rendered four-pixel outset plus label alignment across responsive sizes and themes." }, "entries": { - "tanstack": "benchmarks/conformance/cases/119-stacked-bar-band-cursor/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/119-stacked-bar-band-cursor/plot.ts" - } + "example": "benchmarks/conformance/cases/119-stacked-bar-band-cursor/example.tsx" } }, { @@ -10183,11 +9743,7 @@ "maintain": "Preserve stable date keys across range changes, motion with reduced-motion fallback, three-label 288 by 192 preview readability, and complete visual ownership through theme tokens rather than custom SVG overlays." }, "entries": { - "tanstack": "benchmarks/conformance/cases/120-themed-interactive-area/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/120-themed-interactive-area/recharts.ts" - } + "example": "benchmarks/conformance/cases/120-themed-interactive-area/example.tsx" } }, { @@ -10304,11 +9860,7 @@ "maintain": "Preserve 24 stable day keys, metric-specific totals and tooltips, keyboard-operable selectors, reduced-motion behavior, and one native chart definition rather than drawing the active state in React." }, "entries": { - "tanstack": "benchmarks/conformance/cases/121-active-bar-dashboard/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/121-active-bar-dashboard/recharts.ts" - } + "example": "benchmarks/conformance/cases/121-active-bar-dashboard/example.tsx" } }, { @@ -10364,11 +9916,7 @@ "maintain": "Preserve the three stable keyed series, spring continuity across revisions, reduced-motion behavior, inherited light and dark tokens, restrained metric copy, and responsive three-surface preview." }, "entries": { - "tanstack": "benchmarks/conformance/cases/122-premium-kpi-sparklines/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/122-premium-kpi-sparklines/plot.ts" - } + "example": "benchmarks/conformance/cases/122-premium-kpi-sparklines/example.tsx" } }, { @@ -10488,11 +10036,7 @@ "maintain": "Preserve stable browser identities, native pie allocation, CSS-variable colors, the seven-arc composition, synchronized center and legend state, spring continuity, and reduced-motion behavior without custom SVG paths." }, "entries": { - "tanstack": "benchmarks/conformance/cases/123-active-donut-metric/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/123-active-donut-metric/recharts.ts" - } + "example": "benchmarks/conformance/cases/123-active-donut-metric/example.tsx" } }, { @@ -10535,11 +10079,7 @@ "maintain": "Preserve one shared row array per revision, three real SVG chart surfaces, stable datum keys, palette-scoped CSS variables with light-dark fallbacks, unique resource prefixes, reduced-motion handling, and matching geometry across treatments." }, "entries": { - "tanstack": "benchmarks/conformance/cases/124-theme-palette-matrix/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/124-theme-palette-matrix/plot.ts" - } + "example": "benchmarks/conformance/cases/124-theme-palette-matrix/example.tsx" } }, { @@ -10580,11 +10120,7 @@ "maintain": "Keep stage order, semantic keys, proportional widths, centered extents, direct labels, raw values, and the tapered final edge stable across responsive sizes and data revisions." }, "entries": { - "tanstack": "benchmarks/conformance/cases/125-sales-funnel/tanstack.ts", - "reference": { - "renderer": "observable-plot", - "path": "benchmarks/conformance/cases/125-sales-funnel/plot.ts" - } + "example": "benchmarks/conformance/cases/125-sales-funnel/example.tsx" } }, { @@ -10624,11 +10160,7 @@ "maintain": "Keep the full flat hierarchy as source data, preserve canonical node IDs and ancestry across root changes, leave navigation state and the center back control in the application, and keep motion centered by interpolating sector angles and radii before regenerating each path." }, "entries": { - "tanstack": "benchmarks/conformance/cases/126-drillable-sunburst/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/126-drillable-sunburst/recharts.ts" - } + "example": "benchmarks/conformance/cases/126-drillable-sunburst/example.tsx" } }, { @@ -10667,11 +10199,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/127-shadcn-dashboard/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/127-shadcn-dashboard/recharts.ts" - } + "example": "benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx" } }, { @@ -10681,17 +10209,11 @@ "height": 600, "order": 1290, "id": "128-shadcn-bar-multiple", - "title": "shadcn bar chart – multiple", + "title": "shadcn Bar Chart - Multiple", "family": "bar", - "intent": "Reproduce shadcn/ui's grouped bar example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-bar-multiple example with TanStack Charts.", "support": "native", - "features": [ - "grouped bars", - "rounded corners", - "horizontal grid", - "grouped tooltip", - "shadcn card shell" - ], + "features": ["bar chart", "multiple", "shadcn card shell"], "geometry": [ { "role": "bar", @@ -10704,15 +10226,11 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-bar-multiple card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream data, grouped layout, axis treatment, tooltip labels, card dimensions, footer, and light and dark tokens." + "maintain": "Preserve the pinned upstream data, bar multiple treatment, card dimensions, and light and dark tokens." }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/128-shadcn-bar-multiple/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/128-shadcn-bar-multiple/recharts.ts" - } + "example": "benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx" } }, { @@ -10722,25 +10240,15 @@ "height": 600, "order": 1300, "id": "129-shadcn-pie-donut-text", - "title": "shadcn pie chart – donut with text", + "title": "shadcn Pie Chart - Donut with Text", "family": "pie", - "intent": "Reproduce shadcn/ui's donut-with-center-text example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-pie-donut-text example with TanStack Charts.", "support": "native", - "features": [ - "donut chart", - "center text", - "sector separators", - "tooltip", - "shadcn card shell" - ], + "features": ["pie chart", "donut text", "shadcn card shell"], "geometry": [ { "role": "arc", "count": 5 - }, - { - "role": "text", - "count": 2 } ], "source": { @@ -10749,15 +10257,11 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-pie-donut-text card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream browser data, donut radii, separators, center total and label, tooltip, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, pie donut text treatment, card dimensions, and light and dark tokens." }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/129-shadcn-pie-donut-text/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/129-shadcn-pie-donut-text/recharts.ts" - } + "example": "benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx" } }, { @@ -10767,18 +10271,11 @@ "height": 600, "order": 1310, "id": "130-shadcn-radar-multiple", - "title": "shadcn radar chart – multiple", + "title": "shadcn Radar Chart - Multiple", "family": "radar", - "intent": "Reproduce shadcn/ui's multi-series radar example while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-radar-multiple example with TanStack Charts.", "support": "native", - "features": [ - "radar chart", - "multiple series", - "polygon grid", - "angle labels", - "grouped tooltip", - "shadcn card shell" - ], + "features": ["radar chart", "multiple", "shadcn card shell"], "geometry": [ { "role": "radar", @@ -10791,15 +10288,11 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-radar-multiple card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream data, polygon guide, angle labels, two filled profiles, tooltip, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, radar multiple treatment, card dimensions, and light and dark tokens." }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/130-shadcn-radar-multiple/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/130-shadcn-radar-multiple/recharts.ts" - } + "example": "benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx" } }, { @@ -10809,25 +10302,19 @@ "height": 600, "order": 1320, "id": "131-shadcn-radial-text", - "title": "shadcn radial chart – text", + "title": "shadcn Radial Chart - Text", "family": "radial", - "intent": "Reproduce shadcn/ui's partial radial chart with center text while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-radial-text example with TanStack Charts.", "support": "native", - "features": [ - "partial radial bar", - "rounded caps", - "background track", - "center text", - "shadcn card shell" - ], + "features": ["radial chart", "text", "shadcn card shell"], "geometry": [ { - "role": "arc", - "count": 2 - }, - { - "role": "text", - "count": 2 + "role": "bar", + "count": 1, + "rendererRoles": { + "recharts": "arc", + "tanstack": "arc" + } } ], "source": { @@ -10836,15 +10323,11 @@ }, "ai": { "create": "Reproduce the official shadcn/ui chart-radial-text card and data with TanStack Charts.", - "maintain": "Preserve the pinned upstream value, 250-degree sweep, 80/90 radii, rounded track, center text, card dimensions, footer, and themes." + "maintain": "Preserve the pinned upstream data, radial text treatment, card dimensions, and light and dark tokens." }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/131-shadcn-radial-text/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/131-shadcn-radial-text/recharts.ts" - } + "example": "benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx" } }, { @@ -10854,22 +10337,15 @@ "height": 600, "order": 1330, "id": "132-shadcn-tooltip-advanced", - "title": "shadcn tooltip – advanced", + "title": "shadcn Tooltip - Advanced", "family": "tooltip", - "intent": "Reproduce shadcn/ui's stacked-bar card and custom total tooltip while replacing Recharts with TanStack Charts.", + "intent": "Reproduce shadcn/ui's chart-tooltip-advanced example with TanStack Charts.", "support": "native", - "features": [ - "stacked bars", - "rounded corners", - "rich tooltip body", - "computed total", - "default focus", - "shadcn card shell" - ], + "features": ["tooltip chart", "advanced", "shadcn card shell"], "geometry": [ { "role": "bar", - "count": 12 + "count": 10 } ], "source": { @@ -10877,16 +10353,12 @@ "url": "https://ui.shadcn.com/view/new-york-v4/chart-tooltip-advanced" }, "ai": { - "create": "Reproduce the official shadcn/ui chart-tooltip-advanced card and data with TanStack Charts and its React tooltip composition entry.", - "maintain": "Preserve the pinned upstream data, stack order, weekday axis, initial focused day, 180-pixel rich tooltip, row labels, kcal units, total, card dimensions, and themes." + "create": "Reproduce the official shadcn/ui chart-tooltip-advanced card and data with TanStack Charts.", + "maintain": "Preserve the pinned upstream data, tooltip advanced treatment, card dimensions, and light and dark tokens." }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/recharts.ts" - } + "example": "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx" } }, { @@ -10917,11 +10389,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/133-shadcn-area-axes/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/133-shadcn-area-axes/recharts.ts" - } + "example": "benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx" } }, { @@ -10952,11 +10420,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/134-shadcn-area-default/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/134-shadcn-area-default/recharts.ts" - } + "example": "benchmarks/conformance/cases/134-shadcn-area-default/example.tsx" } }, { @@ -10987,11 +10451,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/135-shadcn-area-gradient/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/135-shadcn-area-gradient/recharts.ts" - } + "example": "benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx" } }, { @@ -11022,11 +10482,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/136-shadcn-area-icons/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/136-shadcn-area-icons/recharts.ts" - } + "example": "benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx" } }, { @@ -11057,11 +10513,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/137-shadcn-area-interactive/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/137-shadcn-area-interactive/recharts.ts" - } + "example": "benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx" } }, { @@ -11092,11 +10544,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/138-shadcn-area-legend/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/138-shadcn-area-legend/recharts.ts" - } + "example": "benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx" } }, { @@ -11127,11 +10575,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/139-shadcn-area-linear/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/139-shadcn-area-linear/recharts.ts" - } + "example": "benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx" } }, { @@ -11162,11 +10606,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/recharts.ts" - } + "example": "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx" } }, { @@ -11197,11 +10637,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/141-shadcn-area-stacked/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/141-shadcn-area-stacked/recharts.ts" - } + "example": "benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx" } }, { @@ -11232,11 +10668,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/142-shadcn-area-step/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/142-shadcn-area-step/recharts.ts" - } + "example": "benchmarks/conformance/cases/142-shadcn-area-step/example.tsx" } }, { @@ -11267,11 +10699,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/143-shadcn-bar-active/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/143-shadcn-bar-active/recharts.ts" - } + "example": "benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx" } }, { @@ -11302,11 +10730,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/144-shadcn-bar-default/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/144-shadcn-bar-default/recharts.ts" - } + "example": "benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx" } }, { @@ -11337,11 +10761,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/145-shadcn-bar-horizontal/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/145-shadcn-bar-horizontal/recharts.ts" - } + "example": "benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx" } }, { @@ -11372,11 +10792,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/146-shadcn-bar-interactive/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/146-shadcn-bar-interactive/recharts.ts" - } + "example": "benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx" } }, { @@ -11407,11 +10823,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/147-shadcn-bar-label-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/147-shadcn-bar-label-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx" } }, { @@ -11442,11 +10854,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/148-shadcn-bar-label/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/148-shadcn-bar-label/recharts.ts" - } + "example": "benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx" } }, { @@ -11477,11 +10885,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/149-shadcn-bar-mixed/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/149-shadcn-bar-mixed/recharts.ts" - } + "example": "benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx" } }, { @@ -11512,11 +10916,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/150-shadcn-bar-negative/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/150-shadcn-bar-negative/recharts.ts" - } + "example": "benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx" } }, { @@ -11547,11 +10947,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/151-shadcn-bar-stacked/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/151-shadcn-bar-stacked/recharts.ts" - } + "example": "benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx" } }, { @@ -11582,11 +10978,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/152-shadcn-line-default/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/152-shadcn-line-default/recharts.ts" - } + "example": "benchmarks/conformance/cases/152-shadcn-line-default/example.tsx" } }, { @@ -11617,11 +11009,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/153-shadcn-line-dots-colors/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/153-shadcn-line-dots-colors/recharts.ts" - } + "example": "benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx" } }, { @@ -11652,11 +11040,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/154-shadcn-line-dots-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/154-shadcn-line-dots-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx" } }, { @@ -11687,11 +11071,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/155-shadcn-line-dots/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/155-shadcn-line-dots/recharts.ts" - } + "example": "benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx" } }, { @@ -11722,11 +11102,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/156-shadcn-line-interactive/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/156-shadcn-line-interactive/recharts.ts" - } + "example": "benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx" } }, { @@ -11757,11 +11133,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/157-shadcn-line-label-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/157-shadcn-line-label-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx" } }, { @@ -11792,11 +11164,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/158-shadcn-line-label/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/158-shadcn-line-label/recharts.ts" - } + "example": "benchmarks/conformance/cases/158-shadcn-line-label/example.tsx" } }, { @@ -11827,11 +11195,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/159-shadcn-line-linear/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/159-shadcn-line-linear/recharts.ts" - } + "example": "benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx" } }, { @@ -11862,11 +11226,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/160-shadcn-line-multiple/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/160-shadcn-line-multiple/recharts.ts" - } + "example": "benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx" } }, { @@ -11897,11 +11257,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/161-shadcn-line-step/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/161-shadcn-line-step/recharts.ts" - } + "example": "benchmarks/conformance/cases/161-shadcn-line-step/example.tsx" } }, { @@ -11932,11 +11288,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/162-shadcn-pie-donut-active/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/162-shadcn-pie-donut-active/recharts.ts" - } + "example": "benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx" } }, { @@ -11967,11 +11319,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/163-shadcn-pie-donut/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/163-shadcn-pie-donut/recharts.ts" - } + "example": "benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx" } }, { @@ -12002,11 +11350,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/164-shadcn-pie-interactive/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/164-shadcn-pie-interactive/recharts.ts" - } + "example": "benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx" } }, { @@ -12037,11 +11381,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/165-shadcn-pie-label-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/165-shadcn-pie-label-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx" } }, { @@ -12072,11 +11412,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/166-shadcn-pie-label-list/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/166-shadcn-pie-label-list/recharts.ts" - } + "example": "benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx" } }, { @@ -12107,11 +11443,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/167-shadcn-pie-label/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/167-shadcn-pie-label/recharts.ts" - } + "example": "benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx" } }, { @@ -12142,11 +11474,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/168-shadcn-pie-legend/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/168-shadcn-pie-legend/recharts.ts" - } + "example": "benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx" } }, { @@ -12177,11 +11505,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/169-shadcn-pie-separator-none/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/169-shadcn-pie-separator-none/recharts.ts" - } + "example": "benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx" } }, { @@ -12212,11 +11536,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/170-shadcn-pie-simple/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/170-shadcn-pie-simple/recharts.ts" - } + "example": "benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx" } }, { @@ -12247,11 +11567,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/171-shadcn-pie-stacked/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/171-shadcn-pie-stacked/recharts.ts" - } + "example": "benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx" } }, { @@ -12282,11 +11598,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/172-shadcn-radar-default/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/172-shadcn-radar-default/recharts.ts" - } + "example": "benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx" } }, { @@ -12317,11 +11629,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/173-shadcn-radar-dots/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/173-shadcn-radar-dots/recharts.ts" - } + "example": "benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx" } }, { @@ -12352,11 +11660,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/recharts.ts" - } + "example": "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx" } }, { @@ -12387,11 +11691,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/recharts.ts" - } + "example": "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx" } }, { @@ -12422,11 +11722,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/recharts.ts" - } + "example": "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx" } }, { @@ -12457,11 +11753,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx" } }, { @@ -12492,11 +11784,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/recharts.ts" - } + "example": "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx" } }, { @@ -12527,11 +11815,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/179-shadcn-radar-grid-none/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/179-shadcn-radar-grid-none/recharts.ts" - } + "example": "benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx" } }, { @@ -12562,11 +11846,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/180-shadcn-radar-icons/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/180-shadcn-radar-icons/recharts.ts" - } + "example": "benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx" } }, { @@ -12597,11 +11877,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/181-shadcn-radar-label-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/181-shadcn-radar-label-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx" } }, { @@ -12632,11 +11908,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/182-shadcn-radar-legend/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/182-shadcn-radar-legend/recharts.ts" - } + "example": "benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx" } }, { @@ -12667,11 +11939,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/183-shadcn-radar-lines-only/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/183-shadcn-radar-lines-only/recharts.ts" - } + "example": "benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx" } }, { @@ -12702,11 +11970,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/184-shadcn-radar-radius/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/184-shadcn-radar-radius/recharts.ts" - } + "example": "benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx" } }, { @@ -12741,11 +12005,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/185-shadcn-radial-grid/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/185-shadcn-radial-grid/recharts.ts" - } + "example": "benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx" } }, { @@ -12780,11 +12040,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/186-shadcn-radial-label/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/186-shadcn-radial-label/recharts.ts" - } + "example": "benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx" } }, { @@ -12819,11 +12075,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/187-shadcn-radial-shape/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/187-shadcn-radial-shape/recharts.ts" - } + "example": "benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx" } }, { @@ -12858,11 +12110,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/188-shadcn-radial-simple/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/188-shadcn-radial-simple/recharts.ts" - } + "example": "benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx" } }, { @@ -12897,11 +12145,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/189-shadcn-radial-stacked/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/189-shadcn-radial-stacked/recharts.ts" - } + "example": "benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx" } }, { @@ -12932,11 +12176,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/190-shadcn-tooltip-default/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/190-shadcn-tooltip-default/recharts.ts" - } + "example": "benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx" } }, { @@ -12967,11 +12207,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/recharts.ts" - } + "example": "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx" } }, { @@ -13002,11 +12238,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/192-shadcn-tooltip-icons/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/192-shadcn-tooltip-icons/recharts.ts" - } + "example": "benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx" } }, { @@ -13037,11 +12269,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/recharts.ts" - } + "example": "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx" } }, { @@ -13072,11 +12300,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/recharts.ts" - } + "example": "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx" } }, { @@ -13107,11 +12331,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/recharts.ts" - } + "example": "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx" } }, { @@ -13142,11 +12362,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/recharts.ts" - } + "example": "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx" } }, { @@ -13177,11 +12393,7 @@ }, "collection": "shadcn", "entries": { - "tanstack": "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/tanstack.ts", - "reference": { - "renderer": "recharts", - "path": "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/recharts.ts" - } + "example": "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx" } } ] diff --git a/benchmarks/conformance/definition-coverage-roadmap.json b/benchmarks/conformance/definition-coverage-roadmap.json index da0c72c6..793a2a3b 100644 --- a/benchmarks/conformance/definition-coverage-roadmap.json +++ b/benchmarks/conformance/definition-coverage-roadmap.json @@ -91,7 +91,7 @@ "dependsOn": [], "frictionIds": ["F-184"], "evidence": [ - "cases/116-geometry-morph/tanstack.ts", + "cases/116-geometry-morph/example.tsx", "cases/116-geometry-morph/tanstack.test.ts" ] }, @@ -221,7 +221,7 @@ "entryPoint": "@tanstack/charts/stack", "bundleFixture": "benchmarks/entries/charts-transform-stack.ts", "evidence": [ - "cases/26-diverging-likert/tanstack.ts", + "cases/26-diverging-likert/example.tsx", "../../packages/charts-core/src/stack-internal.test.ts", "../entries/charts-transform-stack.ts" ] @@ -254,7 +254,7 @@ "entryPoint": "@tanstack/charts", "bundleFixture": "benchmarks/entries/charts-core.ts", "evidence": [ - "cases/70-composed-chart/tanstack.ts", + "cases/70-composed-chart/example.tsx", "cases/70-composed-chart/tanstack.test.ts", "../../packages/charts-core/src/marks.test.ts", "../../docs/reference/marks/bar-and-rect.md" @@ -272,7 +272,7 @@ "entryPoint": "@tanstack/charts/box", "bundleFixture": "benchmarks/entries/charts-box-svg.ts", "evidence": [ - "cases/15-boxplot/tanstack.ts", + "cases/15-boxplot/example.tsx", "../../packages/charts-core/src/box.test.ts", "../entries/charts-box-svg.ts" ] @@ -305,7 +305,7 @@ "entryPoint": "@tanstack/charts/regression", "bundleFixture": "benchmarks/entries/charts-regression-svg.ts", "evidence": [ - "cases/31-linear-regression/tanstack.ts", + "cases/31-linear-regression/example.tsx", "../../packages/charts-core/src/regression.test.ts", "../entries/charts-regression-svg.ts" ] @@ -322,7 +322,7 @@ "entryPoint": "@tanstack/charts/difference", "bundleFixture": "benchmarks/entries/charts-difference-svg.ts", "evidence": [ - "cases/33-difference-chart/tanstack.ts", + "cases/33-difference-chart/example.tsx", "../../packages/charts-core/src/difference.test.ts", "../entries/charts-difference-svg.ts" ] @@ -339,7 +339,7 @@ "entryPoint": "@tanstack/charts/ridgeline", "bundleFixture": "benchmarks/entries/charts-ridgeline-svg.ts", "evidence": [ - "cases/62-ridgeline-density/tanstack.ts", + "cases/62-ridgeline-density/example.tsx", "cases/62-ridgeline-density/tanstack.test.ts", "../../packages/charts-core/src/ridgeline.test.ts", "../../packages/charts-core/src/mapped-spacing-internal.test.ts", @@ -358,7 +358,7 @@ "entryPoint": "@tanstack/charts/violin", "bundleFixture": "benchmarks/entries/charts-violin-svg.ts", "evidence": [ - "cases/63-violin-distributions/tanstack.ts", + "cases/63-violin-distributions/example.tsx", "cases/63-violin-distributions/tanstack.test.ts", "../../packages/charts-core/src/violin.test.ts", "../../packages/charts-core/src/tick.test.ts", @@ -378,7 +378,7 @@ "entryPoint": "@tanstack/charts/transform/mosaic", "bundleFixture": "benchmarks/entries/charts-transform-mosaic.ts", "evidence": [ - "cases/64-marimekko-mosaic/tanstack.ts", + "cases/64-marimekko-mosaic/example.tsx", "cases/64-marimekko-mosaic/tanstack.test.ts", "../../packages/charts-core/src/transform-mosaic.test.ts", "../../packages/charts-core/src/proportional-interval-internal.test.ts", @@ -446,7 +446,7 @@ "entryPoint": "@tanstack/charts", "bundleFixture": "benchmarks/entries/charts-tick-label-accessors.ts", "evidence": [ - "cases/118-token-usage-calendar/tanstack.ts", + "cases/118-token-usage-calendar/example.tsx", "cases/118-token-usage-calendar/tanstack.test.ts", "../../packages/charts-core/src/scene-layout.test.ts", "../../packages/charts-core/src/facet.test.ts", @@ -466,7 +466,7 @@ "entryPoint": "@tanstack/charts/waffle", "bundleFixture": "benchmarks/entries/charts-waffle-svg.ts", "evidence": [ - "cases/41-waffle-unit-chart/tanstack.ts", + "cases/41-waffle-unit-chart/example.tsx", "../../packages/charts-core/src/waffle.test.ts", "../entries/charts-waffle-svg.ts" ] @@ -483,7 +483,7 @@ "entryPoint": "@tanstack/charts/dodge", "bundleFixture": "benchmarks/entries/charts-dodge-svg.ts", "evidence": [ - "cases/52-beeswarm-dodge/tanstack.ts", + "cases/52-beeswarm-dodge/example.tsx", "../../packages/charts-core/src/dodge.test.ts", "../entries/charts-dodge-svg.ts" ] @@ -500,7 +500,7 @@ "entryPoint": "@tanstack/charts/spatial/delaunay", "bundleFixture": "benchmarks/entries/charts-spatial-delaunay-svg.ts", "evidence": [ - "cases/37-delaunay-network/tanstack.ts", + "cases/37-delaunay-network/example.tsx", "../../packages/charts-core/src/spatial-delaunay.test.ts", "../entries/charts-spatial-delaunay-svg.ts" ] @@ -517,7 +517,7 @@ "entryPoint": "@tanstack/charts/spatial/contour", "bundleFixture": "benchmarks/entries/charts-spatial-contour-svg.ts", "evidence": [ - "cases/38-contour-topography/tanstack.ts", + "cases/38-contour-topography/example.tsx", "../../packages/charts-core/src/spatial-contour.test.ts", "../entries/charts-spatial-contour-svg.ts" ] @@ -534,7 +534,7 @@ "entryPoint": "@tanstack/charts/spatial/density", "bundleFixture": "benchmarks/entries/charts-spatial-density-svg.ts", "evidence": [ - "cases/39-density-contours/tanstack.ts", + "cases/39-density-contours/example.tsx", "../../packages/charts-core/src/spatial-density.test.ts", "../entries/charts-spatial-density-svg.ts" ] @@ -551,7 +551,7 @@ "entryPoint": "@tanstack/charts/spatial/hexbin", "bundleFixture": "benchmarks/entries/charts-spatial-hexbin-svg.ts", "evidence": [ - "cases/43-hexbin-density/tanstack.ts", + "cases/43-hexbin-density/example.tsx", "../../packages/charts-core/src/spatial-hexbin.test.ts", "../entries/charts-spatial-hexbin-svg.ts" ] @@ -568,7 +568,7 @@ "entryPoint": "@tanstack/charts/spatial/voronoi", "bundleFixture": "benchmarks/entries/charts-spatial-voronoi-svg.ts", "evidence": [ - "cases/65-voronoi-nearest-tooltip/tanstack.ts", + "cases/65-voronoi-nearest-tooltip/example.tsx", "../../packages/charts-core/src/spatial-voronoi.test.ts", "../entries/charts-spatial-voronoi-svg.ts" ] @@ -585,7 +585,7 @@ "entryPoint": "@tanstack/charts/hierarchy/tree", "bundleFixture": "benchmarks/entries/charts-hierarchy-tree.ts", "evidence": [ - "cases/36-hierarchy-tree/tanstack.ts", + "cases/36-hierarchy-tree/example.tsx", "../../packages/charts-core/src/hierarchy-tree.test.ts", "../entries/charts-hierarchy-tree.ts" ] @@ -602,7 +602,7 @@ "entryPoint": "@tanstack/charts/hierarchy/treemap", "bundleFixture": "benchmarks/entries/charts-hierarchy-treemap.ts", "evidence": [ - "cases/74-recharts-treemap/tanstack.ts", + "cases/74-recharts-treemap/example.tsx", "../../packages/charts-core/src/hierarchy-treemap.test.ts", "../entries/charts-hierarchy-treemap.ts" ] @@ -619,8 +619,8 @@ "entryPoint": "@tanstack/charts/hierarchy/sunburst", "bundleFixture": "benchmarks/entries/charts-hierarchy-sunburst.ts", "evidence": [ - "cases/101-sunburst/tanstack.ts", - "cases/126-drillable-sunburst/tanstack.ts", + "cases/101-sunburst/example.tsx", + "cases/126-drillable-sunburst/example.tsx", "../../packages/charts-core/src/hierarchy-sunburst.test.ts", "../entries/charts-hierarchy-sunburst.ts" ] @@ -637,7 +637,7 @@ "entryPoint": "@tanstack/charts/network/force", "bundleFixture": "benchmarks/entries/charts-network-force.ts", "evidence": [ - "cases/40-force-directed-network/tanstack.ts", + "cases/40-force-directed-network/example.tsx", "../../packages/charts-core/src/network-force.test.ts", "../entries/charts-network-force.ts" ] @@ -654,8 +654,8 @@ "entryPoint": "@tanstack/charts/network/sankey", "bundleFixture": "benchmarks/entries/charts-network-sankey.ts", "evidence": [ - "cases/111-basic-sankey/tanstack.ts", - "cases/111-sankey-flow/tanstack.ts", + "cases/111-basic-sankey/example.tsx", + "cases/111-sankey-flow/example.tsx", "../../packages/charts-core/src/network-sankey.test.ts", "../entries/charts-network-sankey.ts" ] @@ -672,7 +672,7 @@ "entryPoint": "@tanstack/charts/legend", "bundleFixture": "benchmarks/entries/charts-interactive-legend.ts", "evidence": [ - "cases/81-recharts-interactive-legend/tanstack.ts", + "cases/81-recharts-interactive-legend/example.tsx", "cases/81-recharts-interactive-legend/tanstack.test.ts", "../../packages/charts-core/src/interactive-legend.test.ts", "../entries/charts-interactive-legend.ts" @@ -690,7 +690,7 @@ "entryPoint": "@tanstack/charts/selection", "bundleFixture": "benchmarks/entries/charts-keyed-selection.ts", "evidence": [ - "cases/82-chart-table-selection/tanstack.ts", + "cases/82-chart-table-selection/example.tsx", "cases/82-chart-table-selection/tanstack.test.ts", "../../packages/charts-core/src/selection.test.ts" ] @@ -729,7 +729,7 @@ "../../packages/charts-core/src/crosshair.test.ts", "../../packages/charts-core/src/svg-surface.test.ts", "../entries/charts-crosshair-kernel.ts", - "cases/119-stacked-bar-band-cursor/chart.ts", + "cases/119-stacked-bar-band-cursor/example.tsx", "cases/119-stacked-bar-band-cursor/model.test.ts" ] }, @@ -816,14 +816,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/01-line-gaps/tanstack.ts"], + "evidence": ["cases/01-line-gaps/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Native line channels treat null values as discontinuities; scales, path segmentation, and focus lineage remain inside the ordinary definition." } ] @@ -835,14 +835,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/02-multi-line-end-labels/tanstack.ts"], + "evidence": ["cases/02-multi-line-end-labels/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Select each series endpoint through the public grouped select transform." } ] @@ -854,14 +854,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/03-temperature-range-band/tanstack.ts"], + "evidence": ["cases/03-temperature-range-band/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose one native interval area and two native lines over the same temperature observations; no range DTO or custom path is needed." } ] @@ -873,14 +873,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/04-stacked-time-area/tanstack.ts"], + "evidence": ["cases/04-stacked-time-area/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Let native area stacking, color grouping, rule, scales, guides, and legend consume tidy industry rows directly." } ] @@ -892,14 +892,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/13-interval-timeline/tanstack.ts"], + "evidence": ["cases/13-interval-timeline/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Express open-to-close intervals with native barX endpoint channels and an ordinary categorical band." } ] @@ -912,7 +912,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/14-error-bars/tanstack.ts", + "cases/14-error-bars/example.tsx", "cases/14-error-bars/tanstack.test.ts" ], "work": [ @@ -921,7 +921,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Group contributing observations once through public mean and sample-deviation reducers with direct source lineage." }, { @@ -929,7 +929,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the one-deviation estimator, singleton fallback, and endpoint arithmetic as authored statistical meaning." } ] @@ -942,7 +942,7 @@ "status": "verified", "capabilities": ["composite-mark", "box-mark"], "evidence": [ - "cases/15-boxplot/tanstack.ts", + "cases/15-boxplot/example.tsx", "cases/15-boxplot/tanstack.test.ts", "../../packages/charts-core/src/box.test.ts", "../entries/charts-box-svg.ts" @@ -965,14 +965,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/16-lollipop/tanstack.ts"], + "evidence": ["cases/16-lollipop/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native links from zero to value with source-bound dots; no lollipop-specific mark or prepared geometry is warranted." } ] @@ -984,14 +984,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/17-dumbbell/tanstack.ts"], + "evidence": ["cases/17-dumbbell/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose one native endpoint link and two source-bound dot layers for each city row." } ] @@ -1003,14 +1003,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/18-cumulative-histogram/tanstack.ts"], + "evidence": ["cases/18-cumulative-histogram/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public binX and cumulative transforms before an ordinary rect mark while preserving transform lineage." } ] @@ -1022,14 +1022,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/19-moving-average-line/tanstack.ts"], + "evidence": ["cases/19-moving-average-line/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compute the authored fourteen-day means through the public window transform and feed its rows to native lines." } ] @@ -1041,14 +1041,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/20-normalized-stacked-area/tanstack.ts"], + "evidence": ["cases/20-normalized-stacked-area/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use the public normalized stack layout directly on native areaY with ordinary scales, guides, and legend." } ] @@ -1061,7 +1061,7 @@ "status": "verified", "capabilities": ["inside-out-wiggle-stack"], "evidence": [ - "cases/21-streamgraph/tanstack.ts", + "cases/21-streamgraph/example.tsx", "cases/21-streamgraph/tanstack.test.ts", "../../packages/charts-core/src/stack-internal.test.ts", "../entries/charts-transform-stack.ts" @@ -1086,7 +1086,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/22-bollinger-band/tanstack.ts", + "cases/22-bollinger-band/example.tsx", "cases/22-bollinger-band/tanstack.test.ts" ], "work": [ @@ -1095,7 +1095,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compute the full trailing mean and sample deviation once through the public window transform with direct source lineage." }, { @@ -1103,7 +1103,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the twenty-day scope, two-deviation multiplier, and lower/upper channel arithmetic as authored financial meaning." } ] @@ -1115,14 +1115,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/24-quantitative-binned-heatmap/tanstack.ts"], + "evidence": ["cases/24-quantitative-binned-heatmap/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public binXY for authored thresholds and count reduction, then render its semantic intervals with native rect cells." } ] @@ -1134,14 +1134,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/25-calendar-heatmap/tanstack.ts"], + "evidence": ["cases/25-calendar-heatmap/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Keep calendar week and weekday accessors as authored semantics while native cells, band scales, color, and legend own geometry." } ] @@ -1154,7 +1154,7 @@ "status": "verified", "capabilities": ["anchored-stack"], "evidence": [ - "cases/26-diverging-likert/tanstack.ts", + "cases/26-diverging-likert/example.tsx", "cases/26-diverging-likert/tanstack.test.ts", "../../packages/charts-core/src/stack-internal.test.ts", "../entries/charts-transform-stack.ts" @@ -1192,7 +1192,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["selection.ts", "tanstack.ts"], + "sources": ["selection.ts", "example.tsx"], "summary": "Retain event order, metric direction, and last-row country selection." } ] @@ -1204,14 +1204,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/28-candlestick/tanstack.ts"], + "evidence": ["cases/28-candlestick/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native high-low and open-close links from source rows; gain/loss partitioning is presentation policy, not a layout engine." } ] @@ -1224,7 +1224,7 @@ "status": "verified", "capabilities": ["waterfall-layout"], "evidence": [ - "cases/29-waterfall/tanstack.ts", + "cases/29-waterfall/example.tsx", "cases/29-waterfall/tanstack.test.ts", "../../packages/charts-core/src/transform-waterfall.test.ts", "../entries/charts-transform-waterfall.ts" @@ -1262,7 +1262,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["selection.ts", "tanstack.ts"], + "sources": ["selection.ts", "example.tsx"], "summary": "Retain field-to-year display mapping and final-endpoint label policy." } ] @@ -1275,7 +1275,7 @@ "status": "verified", "capabilities": ["composite-mark", "line-x-mark", "regression-mark"], "evidence": [ - "cases/31-linear-regression/tanstack.ts", + "cases/31-linear-regression/example.tsx", "cases/31-linear-regression/tanstack.test.ts", "../../packages/charts-core/src/regression.test.ts", "../entries/charts-regression-svg.ts" @@ -1298,14 +1298,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/32-change-arrows/tanstack.ts"], + "evidence": ["cases/32-change-arrows/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use native arrow endpoint channels for the two authored direction classes without precomputed paths." } ] @@ -1323,7 +1323,7 @@ "difference-mark" ], "evidence": [ - "cases/33-difference-chart/tanstack.ts", + "cases/33-difference-chart/example.tsx", "cases/33-difference-chart/tanstack.test.ts", "../../packages/charts-core/src/difference.test.ts", "../entries/charts-difference-svg.ts" @@ -1346,14 +1346,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/34-pointer-tooltip/tanstack.ts"], + "evidence": ["cases/34-pointer-tooltip/example.tsx"], "work": [ { "kind": "interaction-controller", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "resolved-plot", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Configure point focus, decorative line geometry, keyboard operation, and native point tooltip in the chart definition; host probing remains conformance tooling." } ] @@ -1365,14 +1365,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/35-grouped-tooltip/tanstack.ts"], + "evidence": ["cases/35-grouped-tooltip/example.tsx"], "work": [ { "kind": "interaction-controller", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "resolved-plot", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Configure grouped focus, decorative lines, ordered grouped tooltip content, and portal placement through native definition behavior." } ] @@ -1385,7 +1385,7 @@ "status": "verified", "capabilities": ["hierarchy-tree"], "evidence": [ - "cases/36-hierarchy-tree/tanstack.ts", + "cases/36-hierarchy-tree/example.tsx", "../../packages/charts-core/src/hierarchy-tree.test.ts", "../entries/charts-hierarchy-tree.ts" ], @@ -1409,7 +1409,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "spatial-delaunay"], "evidence": [ - "cases/37-delaunay-network/tanstack.ts", + "cases/37-delaunay-network/example.tsx", "../../packages/charts-core/src/spatial-delaunay.test.ts", "../entries/charts-spatial-delaunay-svg.ts" ], @@ -1433,7 +1433,7 @@ "status": "verified", "capabilities": ["spatial-contour"], "evidence": [ - "cases/38-contour-topography/tanstack.ts", + "cases/38-contour-topography/example.tsx", "../../packages/charts-core/src/spatial-contour.test.ts", "../entries/charts-spatial-contour-svg.ts" ], @@ -1465,7 +1465,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "spatial-density"], "evidence": [ - "cases/39-density-contours/tanstack.ts", + "cases/39-density-contours/example.tsx", "../../packages/charts-core/src/spatial-density.test.ts", "../entries/charts-spatial-density-svg.ts" ], @@ -1489,7 +1489,7 @@ "status": "verified", "capabilities": ["network-force"], "evidence": [ - "cases/40-force-directed-network/tanstack.ts", + "cases/40-force-directed-network/example.tsx", "../../packages/charts-core/src/network-force.test.ts", "../entries/charts-network-force.ts" ], @@ -1512,14 +1512,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/40-geojson-map/tanstack.ts"], + "evidence": ["cases/40-geojson-map/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Pass GeoJSON features and a public geoIdentity projection descriptor to geoShape; Charts owns fitting and path rendering." } ] @@ -1532,7 +1532,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "waffle-mark"], "evidence": [ - "cases/41-waffle-unit-chart/tanstack.ts", + "cases/41-waffle-unit-chart/example.tsx", "../../packages/charts-core/src/waffle.test.ts", "../entries/charts-waffle-svg.ts" ], @@ -1555,7 +1555,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/42-vector-field/tanstack.ts", + "cases/42-vector-field/example.tsx", "cases/42-vector-field/tanstack.test.ts" ], "work": [ @@ -1564,7 +1564,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Pass the authored sample grid to native vector channels; Charts owns one rendered vector and interaction point per source observation." } ] @@ -1577,7 +1577,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "spatial-hexbin"], "evidence": [ - "cases/43-hexbin-density/tanstack.ts", + "cases/43-hexbin-density/example.tsx", "../../packages/charts-core/src/spatial-hexbin.test.ts", "../entries/charts-spatial-hexbin-svg.ts" ], @@ -1599,14 +1599,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/44-framed-scatter/tanstack.ts"], + "evidence": ["cases/44-framed-scatter/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose the native frame and dot marks over configured Cartesian scales without custom border geometry." } ] @@ -1619,7 +1619,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/50-empirical-cdf/tanstack.ts", + "cases/50-empirical-cdf/example.tsx", "cases/50-empirical-cdf/tanstack.test.ts" ], "work": [ @@ -1628,7 +1628,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public rank with explicit tied-rank probability mapping, then render the ordered rows with a native stepped line." } ] @@ -1641,7 +1641,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/51-faceted-distributions/tanstack.ts", + "cases/51-faceted-distributions/example.tsx", "cases/51-faceted-distributions/tanstack.test.ts" ], "work": [ @@ -1650,7 +1650,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public binning, facet-local normalization, and facet marks." } ] @@ -1663,7 +1663,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "dodge-layout"], "evidence": [ - "cases/52-beeswarm-dodge/tanstack.ts", + "cases/52-beeswarm-dodge/example.tsx", "../../packages/charts-core/src/dodge.test.ts", "../entries/charts-dodge-svg.ts" ], @@ -1686,7 +1686,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/53-log-scale-scatter/tanstack.ts", + "cases/53-log-scale-scatter/example.tsx", "cases/53-log-scale-scatter/tanstack.test.ts" ], "work": [ @@ -1695,7 +1695,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Keep the positive-domain guard explicit and feed source rows directly to a native dot mark with a configured log scale." } ] @@ -1708,7 +1708,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/54-bump-ranking/tanstack.ts", + "cases/54-bump-ranking/example.tsx", "cases/54-bump-ranking/tanstack.test.ts" ], "work": [ @@ -1717,7 +1717,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public ranking and order-independent maximum-date selection with native line, dot, and text marks." } ] @@ -1730,7 +1730,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/55-indexed-multi-line/tanstack.ts", + "cases/55-indexed-multi-line/example.tsx", "cases/55-indexed-multi-line/tanstack.test.ts" ], "work": [ @@ -1739,7 +1739,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public first-value normalization and grouped endpoint selection." } ] @@ -1752,7 +1752,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/56-connected-scatter/tanstack.ts", + "cases/56-connected-scatter/example.tsx", "cases/56-connected-scatter/tanstack.test.ts" ], "work": [ @@ -1761,7 +1761,7 @@ "stage": "before-definition", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use the public two-row window output with native arrows." }, { @@ -1769,7 +1769,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the editorial choice of which direction segments receive arrows." } ] @@ -1782,7 +1782,7 @@ "status": "verified", "capabilities": ["view-composition"], "evidence": [ - "cases/57-scatter-marginal-histograms/tanstack.ts", + "cases/57-scatter-marginal-histograms/example.tsx", "cases/57-scatter-marginal-histograms/tanstack.test.ts", "../../packages/charts-core/src/view.test.ts", "../../packages/charts-core/src/scene-embed-internal.test.ts" @@ -1804,7 +1804,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public binX and binY transforms for the two marginal distributions." } ] @@ -1817,7 +1817,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/58-select-extrema/tanstack.ts", + "cases/58-select-extrema/example.tsx", "cases/58-select-extrema/tanstack.test.ts" ], "work": [ @@ -1826,7 +1826,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Feed public minimum and maximum selections as original rows directly to separate dot and text marks." } ] @@ -1838,14 +1838,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/59-grouped-reducer-bars/tanstack.ts"], + "evidence": ["cases/59-grouped-reducer-bars/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public groupBy mean reduction and feed its rows directly to native bars and labels." } ] @@ -1857,14 +1857,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/60-lag-autocorrelation/tanstack.ts"], + "evidence": ["cases/60-lag-autocorrelation/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public two-row window output for lag pairs and compose native dots with an authored identity reference line." } ] @@ -1877,7 +1877,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/61-quantile-ribbon/tanstack.ts", + "cases/61-quantile-ribbon/example.tsx", "cases/61-quantile-ribbon/tanstack.test.ts" ], "work": [ @@ -1886,7 +1886,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Feed public grouped quantile reducers directly to area and line marks." } ] @@ -1899,7 +1899,7 @@ "status": "verified", "capabilities": ["ridgeline-mark"], "evidence": [ - "cases/62-ridgeline-density/tanstack.ts", + "cases/62-ridgeline-density/example.tsx", "cases/62-ridgeline-density/tanstack.test.ts", "../../packages/charts-core/src/ridgeline.test.ts" ], @@ -1909,7 +1909,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public grouped binX and max normalization while retaining nested raw lineage." }, { @@ -1933,7 +1933,7 @@ "status": "verified", "capabilities": ["violin-mark"], "evidence": [ - "cases/63-violin-distributions/tanstack.ts", + "cases/63-violin-distributions/example.tsx", "cases/63-violin-distributions/tanstack.test.ts", "../../packages/charts-core/src/violin.test.ts" ], @@ -1943,7 +1943,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public grouped binY, max normalization, and grouped median reduction while retaining nested raw lineage." }, { @@ -1968,7 +1968,7 @@ "status": "verified", "capabilities": ["mosaic-layout"], "evidence": [ - "cases/64-marimekko-mosaic/tanstack.ts", + "cases/64-marimekko-mosaic/example.tsx", "cases/64-marimekko-mosaic/tanstack.test.ts", "../../packages/charts-core/src/transform-mosaic.test.ts", "../entries/charts-transform-mosaic.ts" @@ -1990,7 +1990,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Keep count policy explicit through groupBy, derive labels by select, and render the resulting cells with ordinary rect and text marks." } ] @@ -2003,7 +2003,7 @@ "status": "verified", "capabilities": ["spatial-voronoi"], "evidence": [ - "cases/65-voronoi-nearest-tooltip/tanstack.ts", + "cases/65-voronoi-nearest-tooltip/example.tsx", "../../packages/charts-core/src/spatial-voronoi.test.ts", "../entries/charts-spatial-voronoi-svg.ts" ], @@ -2027,7 +2027,7 @@ "status": "verified", "capabilities": ["bar-max-thickness"], "evidence": [ - "cases/70-composed-chart/tanstack.ts", + "cases/70-composed-chart/example.tsx", "cases/70-composed-chart/tanstack.test.ts" ], "work": [ @@ -2049,7 +2049,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/71-recharts-population-pyramid/tanstack.ts", + "cases/71-recharts-population-pyramid/example.tsx", "cases/71-recharts-population-pyramid/tanstack.test.ts" ], "work": [ @@ -2058,7 +2058,7 @@ "stage": "before-definition", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public groupBy counts, a signed value accessor, and one native diverging bar stack while retaining raw-observation lineage." } ] @@ -2070,14 +2070,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/72-recharts-mixed-bars/tanstack.ts"], + "evidence": ["cases/72-recharts-mixed-bars/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native grouped and stacked bar intervals with one public group layout; no case-owned slot coordinates are needed." } ] @@ -2090,7 +2090,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/73-many-point-scatter/tanstack.ts", + "cases/73-many-point-scatter/example.tsx", "cases/73-many-point-scatter/tanstack.test.ts" ], "work": [ @@ -2099,7 +2099,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Feed selected source rows to one native dot mark with explicit stable keys and a configured radius scale." } ] @@ -2112,7 +2112,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "hierarchy-treemap"], "evidence": [ - "cases/74-recharts-treemap/tanstack.ts", + "cases/74-recharts-treemap/example.tsx", "../../packages/charts-core/src/hierarchy-treemap.test.ts", "../entries/charts-hierarchy-treemap.ts" ], @@ -2150,7 +2150,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["selection.ts", "tanstack.ts"], + "sources": ["selection.ts", "example.tsx"], "summary": "Retain event order, timed-event direction, first-profile selection, the same-reference 1.1px angle-label offset, and compact margins; angleGrid already owns label context plus default anchor and baseline, so no shared helper is warranted." } ] @@ -2238,7 +2238,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/80-echarts-axis-pointer/tanstack.ts", + "cases/80-echarts-axis-pointer/example.tsx", "cases/80-echarts-axis-pointer/tanstack.test.ts", "../../packages/charts-core/src/focus-guide.test.ts", "../../packages/charts-core/src/runtime.test.ts" @@ -2249,7 +2249,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose grouped x focus, native tooltip content and placement, keyboard policy, continuous snap reach, and the categorical legend in the definition." }, { @@ -2270,7 +2270,7 @@ "status": "verified", "capabilities": ["controlled-signals", "interactive-legend"], "evidence": [ - "cases/81-recharts-interactive-legend/tanstack.ts", + "cases/81-recharts-interactive-legend/example.tsx", "cases/81-recharts-interactive-legend/tanstack.test.ts", "../../packages/charts-core/src/interactive-legend.test.ts", "cases/81-recharts-interactive-legend/view.tsx" @@ -2292,7 +2292,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the controlled visibility snapshot and surrounding conformance region." } ] @@ -2305,7 +2305,7 @@ "status": "verified", "capabilities": ["controlled-signals", "keyed-selection"], "evidence": [ - "cases/82-chart-table-selection/tanstack.ts", + "cases/82-chart-table-selection/example.tsx", "cases/82-chart-table-selection/tanstack.test.ts", "../../packages/charts-core/src/selection.test.ts", "cases/82-chart-table-selection/view.tsx" @@ -2328,7 +2328,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the selected snapshot, semantic HTML table, status, clear action, and persistence as application UI." }, { @@ -2354,7 +2354,7 @@ "brush-x" ], "evidence": [ - "cases/83-focus-context-window/tanstack.ts", + "cases/83-focus-context-window/example.tsx", "cases/83-focus-context-window/tanstack.test.ts", "../../packages/charts-core/src/interaction-brush.test.ts", "../../packages/charts-core/src/renderer.test.ts", @@ -2388,7 +2388,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the accepted four-month policy, range input, status, persistence, and two warranted chart hosts: the independently sized detail and overview have different rows, domains, axes, margins, and behavior." } ] @@ -2401,7 +2401,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/84-pinned-nested-chart-tooltip/tanstack.ts", + "cases/84-pinned-nested-chart-tooltip/example.tsx", "cases/84-pinned-nested-chart-tooltip/model.test.ts", "../../packages/charts-core/src/runtime.test.ts", "../../packages/charts-core/src/svg-surface.test.ts", @@ -2428,7 +2428,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain representative and same-species cohort policy, close-button presentation, and nested child-chart composition." } ] @@ -2441,7 +2441,7 @@ "status": "accepted-boundary", "capabilities": ["application-boundary"], "evidence": [ - "cases/85-scrollable-resource-lanes/tanstack.ts", + "cases/85-scrollable-resource-lanes/example.tsx", "cases/85-scrollable-resource-lanes/tanstack.test.ts", "cases/85-scrollable-resource-lanes/view.tsx", "DEFINITION-COVERAGE-PLAN.md" @@ -2452,7 +2452,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain native overflow, content-width and scroll-preservation policy, the fixed rail and legend, persistent task summary, and complete schedule alternative." }, { @@ -2460,7 +2460,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["tanstack.ts", "view.tsx"], + "sources": ["example.tsx"], "summary": "Use the chart-owned focused point to reveal an offscreen task in the parent scroll viewport and update the persistent summary." } ] @@ -2484,7 +2484,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["model.ts", "view.tsx"], + "sources": ["model.ts", "example.tsx"], "summary": "Retain data arrival, complete history, locked/latest/all viewport policy, controls, and announcements; share only pure case-local viewport and status helpers." } ] @@ -2497,7 +2497,7 @@ "status": "verified", "capabilities": ["view-composition", "focus-guide", "decorative-mark"], "evidence": [ - "cases/87-echarts-synchronized-cursors/tanstack.ts", + "cases/87-echarts-synchronized-cursors/example.tsx", "cases/87-echarts-synchronized-cursors/tanstack.test.ts", "../../packages/charts-core/src/view.test.ts", "../../packages/charts-core/src/focus-guide.test.ts", @@ -2509,7 +2509,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose one grouped-x focus lifecycle, sticky pinning, keyboard navigation, and semantic restoration across both child views." }, { @@ -2529,7 +2529,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the cross-view live value summary as an observer of chart-owned focus and selection events." } ] @@ -2548,7 +2548,7 @@ "evidence": [ "DEFINITION-COVERAGE-AUDIT.md", "DEFINITION-COVERAGE-PLAN.md", - "cases/88-echarts-free-cursor/tanstack.ts", + "cases/88-echarts-free-cursor/example.tsx", "cases/88-echarts-free-cursor/tanstack.test.ts", "../../packages/charts-core/src/interaction-cursor.test.ts" ], @@ -2566,7 +2566,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain domain-specific semantic sliders and live status text over the same controlled position." } ] @@ -2585,7 +2585,7 @@ "evidence": [ "DEFINITION-COVERAGE-AUDIT.md", "DEFINITION-COVERAGE-PLAN.md", - "cases/89-brush-range-selection/tanstack.ts", + "cases/89-brush-range-selection/example.tsx", "cases/89-brush-range-selection/tanstack.test.ts", "../../packages/charts-core/src/interaction-brush.test.ts" ], @@ -2604,7 +2604,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["model.ts", "tanstack.ts"], + "sources": ["model.ts", "example.tsx"], "summary": "Retain monthly cohort selection, accepted range state, the live financial summary, and conformance observation." } ] @@ -2624,7 +2624,7 @@ "DEFINITION-COVERAGE-AUDIT.md", "DEFINITION-COVERAGE-PLAN.md", "cases/90-zoomable-time-window/case.json", - "cases/90-zoomable-time-window/tanstack.ts", + "cases/90-zoomable-time-window/example.tsx", "cases/90-zoomable-time-window/tanstack.test.ts", "../../packages/charts-core/src/interaction-zoom.test.ts" ], @@ -2643,7 +2643,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["model.ts", "tanstack.ts"], + "sources": ["model.ts", "example.tsx"], "summary": "Retain accepted-window state, visible-row and y-domain policy, status, reset, conformance observation, and persistence." } ] @@ -2669,12 +2669,12 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts", "model.ts"], + "sources": ["example.tsx", "model.ts"], "summary": "Retain the accepted frame, playback timing, transport controls, status, and announcements." } ], "evidence": [ - "cases/91-timeline-playback-scrubber/tanstack.ts", + "cases/91-timeline-playback-scrubber/example.tsx", "cases/91-timeline-playback-scrubber/tanstack.test.ts", "../../packages/charts-core/src/interaction-handle.test.ts" ] @@ -2700,12 +2700,12 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts", "view.tsx", "model.ts"], + "sources": ["example.tsx", "model.ts"], "summary": "Retain the date input, validation, event constraint, accepted end, commit transaction, status, and exact-value alternative." } ], "evidence": [ - "cases/92-editable-event-range/tanstack.ts", + "cases/92-editable-event-range/example.tsx", "cases/92-editable-event-range/tanstack.test.ts", "../../packages/charts-core/src/interaction-handle.test.ts" ] @@ -2768,7 +2768,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the semantic selected-frequency total and center-label presentation." } ] @@ -2835,7 +2835,7 @@ "status": "verified", "capabilities": ["radial-bars"], "evidence": [ - "cases/97-rose/tanstack.ts", + "cases/97-rose/example.tsx", "cases/97-rose/tanstack.test.ts", "../../packages/charts-core/src/polar-radial-bar.test.ts" ], @@ -2913,7 +2913,7 @@ "stage": "definition-builder", "owner": "case", "coordinateSpace": "none", - "sources": ["selection.ts", "tanstack.ts"], + "sources": ["selection.ts", "example.tsx"], "summary": "Retain event order, metric direction, country filter, first-profile selection, the same-reference 1.1px angle-label offset, and compact margins; angleGrid already owns label context plus default anchor and baseline, so no shared helper is warranted." } ] @@ -2958,7 +2958,7 @@ "status": "verified", "capabilities": ["hierarchy-sunburst"], "evidence": [ - "cases/101-sunburst/tanstack.ts", + "cases/101-sunburst/example.tsx", "cases/101-sunburst/tanstack.test.ts", "../../packages/charts-core/src/hierarchy-sunburst.test.ts" ], @@ -2990,14 +2990,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/102-world-choropleth/tanstack.ts"], + "evidence": ["cases/102-world-choropleth/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose land, joined country features, and sphere through geoShape with a public Equal Earth descriptor and threshold color scale." } ] @@ -3009,14 +3009,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/103-bubble-map/tanstack.ts"], + "evidence": ["cases/103-bubble-map/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose land, source-linked geographic points, and sphere through geoShape with a public projection descriptor and radius channel." } ] @@ -3028,14 +3028,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/104-orthographic-globe/tanstack.ts"], + "evidence": ["cases/104-orthographic-globe/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Layer sphere, graticule, and land through geoShape with one authored orthographic projection descriptor per mark." } ] @@ -3047,14 +3047,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/105-route-map/tanstack.ts"], + "evidence": ["cases/105-route-map/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Layer land, graticule, route, and sphere through geoShape with a shared public Equal Earth projection descriptor." } ] @@ -3066,14 +3066,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/106-polar-line/tanstack.ts"], + "evidence": ["cases/106-polar-line/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native polar grids and a radial line from semantic day-angle and temperature-radius channels." } ] @@ -3085,14 +3085,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/107-polar-scatter/tanstack.ts"], + "evidence": ["cases/107-polar-scatter/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native polar grids and radial dots from semantic wind direction, latitude band, and speed channels." } ] @@ -3104,14 +3104,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/108-country-choropleth/tanstack.ts"], + "evidence": ["cases/108-country-choropleth/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Render joined country features through geoShape with a public Equal Earth descriptor and quantized color definition." } ] @@ -3124,7 +3124,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/109-us-state-choropleth/tanstack.ts", + "cases/109-us-state-choropleth/example.tsx", "cases/109-us-state-choropleth/tanstack.test.ts" ], "work": [ @@ -3133,7 +3133,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Pass county features and the public Albers USA projection descriptor to geoShape; Charts owns fitting, paths, and quantile color mapping." } ] @@ -3146,7 +3146,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/110-projection-gallery/tanstack.ts", + "cases/110-projection-gallery/example.tsx", "cases/110-projection-gallery/tanstack.test.ts", "../../packages/charts-core/src/facet.test.ts" ], @@ -3182,7 +3182,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "network-sankey"], "evidence": [ - "cases/111-basic-sankey/tanstack.ts", + "cases/111-basic-sankey/example.tsx", "cases/111-basic-sankey/tanstack.test.ts", "../../packages/charts-core/src/network-sankey.test.ts" ], @@ -3206,7 +3206,7 @@ "status": "verified", "capabilities": ["resolved-mark-layout", "network-sankey"], "evidence": [ - "cases/111-sankey-flow/tanstack.ts", + "cases/111-sankey-flow/example.tsx", "cases/111-sankey-flow/tanstack.test.ts", "../../packages/charts-core/src/network-sankey.test.ts" ], @@ -3225,7 +3225,7 @@ "stage": "resolved-layout", "owner": "case", "coordinateSpace": "resolved-plot", - "sources": ["tanstack.ts", "layout.ts"], + "sources": ["example.tsx", "layout.ts"], "dependencies": [], "summary": "Retain authored compact wording, label side and offsets, two-line values, backdrop measurement, title placement, and semantic paint." } @@ -3239,7 +3239,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/112-motion-entrance/tanstack.ts", + "cases/112-motion-entrance/example.tsx", "cases/112-motion-entrance/tanstack.test.ts", "shared/motion.ts" ], @@ -3249,7 +3249,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain replay and demo settings while motion stays definition-local." } ] @@ -3262,7 +3262,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/113-motion-updates/tanstack.ts", + "cases/113-motion-updates/example.tsx", "cases/113-motion-updates/tanstack.test.ts", "shared/motion.ts" ], @@ -3272,7 +3272,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain stage controls while keyed update motion stays declarative." } ] @@ -3285,7 +3285,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/114-spring-line-motion/tanstack.ts", + "cases/114-spring-line-motion/example.tsx", "cases/114-spring-line-motion/tanstack.test.ts", "shared/motion.ts" ], @@ -3295,7 +3295,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain demo controls while line morph and spring overrides stay declarative." } ] @@ -3308,7 +3308,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/115-definition-motion/tanstack.ts", + "cases/115-definition-motion/example.tsx", "cases/115-definition-motion/tanstack.test.ts", "shared/motion.ts" ], @@ -3318,7 +3318,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain controls while chart, mark, datum, axis, tick, and label motion stay definition-owned." } ] @@ -3332,7 +3332,7 @@ "capabilities": ["inline-custom-boundary"], "evidence": [ "DEFINITION-COVERAGE-AUDIT.md", - "cases/116-geometry-morph/tanstack.ts", + "cases/116-geometry-morph/example.tsx", "cases/116-geometry-morph/tanstack.test.ts" ], "work": [ @@ -3341,7 +3341,7 @@ "stage": "mark-render", "owner": "case", "coordinateSpace": "resolved-plot", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the deliberately bespoke normalized topology for cross-shape path morphing." } ] @@ -3354,7 +3354,7 @@ "status": "verified", "capabilities": ["focus-guide"], "evidence": [ - "cases/117-focus-cursor-motion/tanstack.ts", + "cases/117-focus-cursor-motion/example.tsx", "cases/117-focus-cursor-motion/tanstack.test.ts", "../../packages/charts-core/src/focus-guide.test.ts", "../../packages/charts-core/src/focus-mark.test.ts", @@ -3377,7 +3377,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the visible aria-live status and update it from host focus events." } ] @@ -3390,7 +3390,7 @@ "status": "verified", "capabilities": ["axis-tick-label-accessors"], "evidence": [ - "cases/118-token-usage-calendar/tanstack.ts", + "cases/118-token-usage-calendar/example.tsx", "cases/118-token-usage-calendar/tanstack.test.ts", "cases/118-token-usage-calendar/shell.test.ts", "../../packages/charts-core/src/scene-layout.test.ts", @@ -3415,7 +3415,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["layout.ts", "shell.tsx"], + "sources": ["layout.ts", "example.tsx"], "summary": "Retain width-derived host sizing and tooltip presentation." } ] @@ -3428,7 +3428,7 @@ "status": "verified", "capabilities": ["crosshair"], "evidence": [ - "cases/119-stacked-bar-band-cursor/chart.ts", + "cases/119-stacked-bar-band-cursor/example.tsx", "cases/119-stacked-bar-band-cursor/model.test.ts", "cases/119-stacked-bar-band-cursor/case.json", "../../packages/charts-core/src/crosshair.test.ts", @@ -3446,7 +3446,7 @@ "packages/charts-core/src/crosshair.ts", "packages/charts-core/src/crosshair-resolver.ts", "packages/charts-core/src/focus-presentation.ts", - "chart.ts" + "example.tsx" ], "summary": "Resolve stable categorical bands, endpoint rules, labels, grouped focus, and keyed motion through renderer-native crosshair guides." }, @@ -3468,8 +3468,8 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/120-themed-interactive-area/chart.ts", - "cases/120-themed-interactive-area/tanstack.ts", + "cases/120-themed-interactive-area/example.tsx", + "cases/120-themed-interactive-area/example.tsx", "cases/120-themed-interactive-area/tanstack.test.ts" ], "work": [ @@ -3478,7 +3478,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["chart.ts"], + "sources": ["example.tsx"], "summary": "Compose the gradient area, line, focus points, sparse guides, tooltip, and keyed spring policy through the current definition API." }, { @@ -3486,7 +3486,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Retain the traffic card, range controls, and selected time-window state as application UI." } ] @@ -3499,7 +3499,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/121-active-bar-dashboard/chart.ts", + "cases/121-active-bar-dashboard/example.tsx", "cases/121-active-bar-dashboard/tanstack.test.ts", "cases/121-active-bar-dashboard/view.tsx" ], @@ -3509,7 +3509,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["chart.ts"], + "sources": ["example.tsx"], "summary": "Compose gradient bars, focus states, the animated band ring, sparse guides, tooltip, and keyed spring updates as one native definition." }, { @@ -3517,7 +3517,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the KPI totals and desktop/mobile selector as accessible application controls." } ] @@ -3530,7 +3530,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/122-premium-kpi-sparklines/chart.ts", + "cases/122-premium-kpi-sparklines/example.tsx", "cases/122-premium-kpi-sparklines/tanstack.test.ts", "cases/122-premium-kpi-sparklines/view.tsx" ], @@ -3540,7 +3540,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["chart.ts"], + "sources": ["example.tsx"], "summary": "Build the three guide-free line and area sparklines from ordinary marks, gradients, CSS-variable paints, and keyed spring motion." }, { @@ -3548,7 +3548,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain metric labels, values, trend annotations, and responsive card layout as application content." } ] @@ -3561,7 +3561,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/123-active-donut-metric/chart.ts", + "cases/123-active-donut-metric/example.tsx", "cases/123-active-donut-metric/tanstack.test.ts", "cases/123-active-donut-metric/view.tsx" ], @@ -3571,7 +3571,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["chart.ts"], + "sources": ["example.tsx"], "summary": "Compose source-linked pie allocation, rounded arcs, an expanded wedge, detached ring, center text, tooltip, and spring motion without custom SVG paths." }, { @@ -3579,7 +3579,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the browser legend buttons and persistent selected-browser policy as accessible application state." } ] @@ -3592,7 +3592,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/124-theme-palette-matrix/chart.ts", + "cases/124-theme-palette-matrix/example.tsx", "cases/124-theme-palette-matrix/tanstack.test.ts", "cases/124-theme-palette-matrix/view.tsx" ], @@ -3602,7 +3602,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["chart.ts"], + "sources": ["example.tsx"], "summary": "Render identical area, comparison line, terminal dot, gradient, and motion geometry through three scoped definition themes." }, { @@ -3610,7 +3610,7 @@ "stage": "before-definition", "owner": "application", "coordinateSpace": "dom", - "sources": ["view.tsx"], + "sources": ["example.tsx"], "summary": "Retain the responsive three-card matrix and palette labels as application composition around real chart surfaces." } ] @@ -3623,7 +3623,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/125-sales-funnel/tanstack.ts", + "cases/125-sales-funnel/example.tsx", "cases/125-sales-funnel/model.test.ts", "cases/125-sales-funnel/case.json" ], @@ -3641,7 +3641,7 @@ "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native areaX groups and direct text labels over explicit linear domains with stable stage keys." } ] @@ -3654,7 +3654,7 @@ "status": "verified", "capabilities": ["hierarchy-sunburst"], "evidence": [ - "cases/126-drillable-sunburst/tanstack.ts", + "cases/126-drillable-sunburst/example.tsx", "cases/126-drillable-sunburst/tanstack.test.ts", "cases/126-drillable-sunburst/model.test.ts", "../../packages/charts-core/src/hierarchy-sunburst.test.ts" @@ -3674,7 +3674,7 @@ "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["model.ts", "center.ts", "tanstack.ts"], + "sources": ["model.ts", "example.tsx"], "summary": "Retain selected-root state, semantic drill selection, the center back control, and conformance observation." } ] @@ -3687,7 +3687,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/127-shadcn-dashboard/tanstack.ts", + "cases/127-shadcn-dashboard/example.tsx", "cases/127-shadcn-dashboard/case.json" ], "work": [ @@ -3697,17 +3697,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx" ], - "summary": "Compose the pinned shadcn dashboard variant through the shared native dashboard chart definition." + "summary": "Define the pinned shadcn dashboard variant as a self-contained native dashboard chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/127-shadcn-dashboard/example.tsx", + "benchmarks/conformance/cases/127-shadcn-dashboard/dashboard.tsx" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3719,7 +3722,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/133-shadcn-area-axes/tanstack.ts", + "cases/133-shadcn-area-axes/example.tsx", "cases/133-shadcn-area-axes/case.json" ], "work": [ @@ -3729,17 +3732,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-axes variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-axes variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/133-shadcn-area-axes/example.tsx", + "benchmarks/conformance/cases/133-shadcn-area-axes/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3751,7 +3757,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/134-shadcn-area-default/tanstack.ts", + "cases/134-shadcn-area-default/example.tsx", "cases/134-shadcn-area-default/case.json" ], "work": [ @@ -3761,17 +3767,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/134-shadcn-area-default/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-default variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-default variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/134-shadcn-area-default/example.tsx", + "benchmarks/conformance/cases/134-shadcn-area-default/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3783,7 +3792,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/135-shadcn-area-gradient/tanstack.ts", + "cases/135-shadcn-area-gradient/example.tsx", "cases/135-shadcn-area-gradient/case.json" ], "work": [ @@ -3793,17 +3802,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-gradient variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-gradient variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/135-shadcn-area-gradient/example.tsx", + "benchmarks/conformance/cases/135-shadcn-area-gradient/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3815,7 +3827,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/136-shadcn-area-icons/tanstack.ts", + "cases/136-shadcn-area-icons/example.tsx", "cases/136-shadcn-area-icons/case.json" ], "work": [ @@ -3825,17 +3837,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-icons variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-icons variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/136-shadcn-area-icons/example.tsx", + "benchmarks/conformance/cases/136-shadcn-area-icons/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3847,7 +3862,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/137-shadcn-area-interactive/tanstack.ts", + "cases/137-shadcn-area-interactive/example.tsx", "cases/137-shadcn-area-interactive/case.json" ], "work": [ @@ -3857,17 +3872,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-interactive variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-interactive variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/137-shadcn-area-interactive/example.tsx", + "benchmarks/conformance/cases/137-shadcn-area-interactive/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3879,7 +3897,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/138-shadcn-area-legend/tanstack.ts", + "cases/138-shadcn-area-legend/example.tsx", "cases/138-shadcn-area-legend/case.json" ], "work": [ @@ -3889,17 +3907,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-legend variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-legend variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/138-shadcn-area-legend/example.tsx", + "benchmarks/conformance/cases/138-shadcn-area-legend/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3911,7 +3932,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/139-shadcn-area-linear/tanstack.ts", + "cases/139-shadcn-area-linear/example.tsx", "cases/139-shadcn-area-linear/case.json" ], "work": [ @@ -3921,17 +3942,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-linear variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-linear variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/139-shadcn-area-linear/example.tsx", + "benchmarks/conformance/cases/139-shadcn-area-linear/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3943,7 +3967,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/140-shadcn-area-stacked-expand/tanstack.ts", + "cases/140-shadcn-area-stacked-expand/example.tsx", "cases/140-shadcn-area-stacked-expand/case.json" ], "work": [ @@ -3953,17 +3977,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-stacked-expand variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-stacked-expand variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/example.tsx", + "benchmarks/conformance/cases/140-shadcn-area-stacked-expand/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -3975,7 +4002,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/141-shadcn-area-stacked/tanstack.ts", + "cases/141-shadcn-area-stacked/example.tsx", "cases/141-shadcn-area-stacked/case.json" ], "work": [ @@ -3985,17 +4012,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-stacked variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-stacked variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/141-shadcn-area-stacked/example.tsx", + "benchmarks/conformance/cases/141-shadcn-area-stacked/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4007,7 +4037,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/142-shadcn-area-step/tanstack.ts", + "cases/142-shadcn-area-step/example.tsx", "cases/142-shadcn-area-step/case.json" ], "work": [ @@ -4017,17 +4047,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/142-shadcn-area-step/example.tsx" ], - "summary": "Compose the pinned shadcn chart-area-step variant through the shared native area chart definition." + "summary": "Define the pinned shadcn chart-area-step variant as a self-contained native area chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/142-shadcn-area-step/example.tsx", + "benchmarks/conformance/cases/142-shadcn-area-step/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4039,7 +4072,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/143-shadcn-bar-active/tanstack.ts", + "cases/143-shadcn-bar-active/example.tsx", "cases/143-shadcn-bar-active/case.json" ], "work": [ @@ -4049,17 +4082,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-active variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-active variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/143-shadcn-bar-active/example.tsx", + "benchmarks/conformance/cases/143-shadcn-bar-active/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4071,7 +4107,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/144-shadcn-bar-default/tanstack.ts", + "cases/144-shadcn-bar-default/example.tsx", "cases/144-shadcn-bar-default/case.json" ], "work": [ @@ -4081,17 +4117,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-default variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-default variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/144-shadcn-bar-default/example.tsx", + "benchmarks/conformance/cases/144-shadcn-bar-default/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4103,7 +4142,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/145-shadcn-bar-horizontal/tanstack.ts", + "cases/145-shadcn-bar-horizontal/example.tsx", "cases/145-shadcn-bar-horizontal/case.json" ], "work": [ @@ -4113,17 +4152,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-horizontal variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-horizontal variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/145-shadcn-bar-horizontal/example.tsx", + "benchmarks/conformance/cases/145-shadcn-bar-horizontal/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4135,7 +4177,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/146-shadcn-bar-interactive/tanstack.ts", + "cases/146-shadcn-bar-interactive/example.tsx", "cases/146-shadcn-bar-interactive/case.json" ], "work": [ @@ -4145,17 +4187,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-interactive variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-interactive variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/146-shadcn-bar-interactive/example.tsx", + "benchmarks/conformance/cases/146-shadcn-bar-interactive/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4167,7 +4212,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/147-shadcn-bar-label-custom/tanstack.ts", + "cases/147-shadcn-bar-label-custom/example.tsx", "cases/147-shadcn-bar-label-custom/case.json" ], "work": [ @@ -4177,17 +4222,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-label-custom variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-label-custom variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/147-shadcn-bar-label-custom/example.tsx", + "benchmarks/conformance/cases/147-shadcn-bar-label-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4199,7 +4247,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/148-shadcn-bar-label/tanstack.ts", + "cases/148-shadcn-bar-label/example.tsx", "cases/148-shadcn-bar-label/case.json" ], "work": [ @@ -4209,17 +4257,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-label variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-label variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/148-shadcn-bar-label/example.tsx", + "benchmarks/conformance/cases/148-shadcn-bar-label/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4231,7 +4282,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/149-shadcn-bar-mixed/tanstack.ts", + "cases/149-shadcn-bar-mixed/example.tsx", "cases/149-shadcn-bar-mixed/case.json" ], "work": [ @@ -4241,17 +4292,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-mixed variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-mixed variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/149-shadcn-bar-mixed/example.tsx", + "benchmarks/conformance/cases/149-shadcn-bar-mixed/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4263,7 +4317,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/128-shadcn-bar-multiple/tanstack.ts", + "cases/128-shadcn-bar-multiple/example.tsx", "cases/128-shadcn-bar-multiple/case.json" ], "work": [ @@ -4273,17 +4327,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-multiple variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-multiple variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/128-shadcn-bar-multiple/example.tsx", + "benchmarks/conformance/cases/128-shadcn-bar-multiple/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4295,7 +4352,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/150-shadcn-bar-negative/tanstack.ts", + "cases/150-shadcn-bar-negative/example.tsx", "cases/150-shadcn-bar-negative/case.json" ], "work": [ @@ -4305,17 +4362,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-negative variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-negative variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/150-shadcn-bar-negative/example.tsx", + "benchmarks/conformance/cases/150-shadcn-bar-negative/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4327,7 +4387,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/151-shadcn-bar-stacked/tanstack.ts", + "cases/151-shadcn-bar-stacked/example.tsx", "cases/151-shadcn-bar-stacked/case.json" ], "work": [ @@ -4337,17 +4397,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx" ], - "summary": "Compose the pinned shadcn chart-bar-stacked variant through the shared native bar chart definition." + "summary": "Define the pinned shadcn chart-bar-stacked variant as a self-contained native bar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/151-shadcn-bar-stacked/example.tsx", + "benchmarks/conformance/cases/151-shadcn-bar-stacked/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4359,7 +4422,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/152-shadcn-line-default/tanstack.ts", + "cases/152-shadcn-line-default/example.tsx", "cases/152-shadcn-line-default/case.json" ], "work": [ @@ -4369,17 +4432,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/152-shadcn-line-default/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-default variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-default variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/152-shadcn-line-default/example.tsx", + "benchmarks/conformance/cases/152-shadcn-line-default/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4391,7 +4457,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/153-shadcn-line-dots-colors/tanstack.ts", + "cases/153-shadcn-line-dots-colors/example.tsx", "cases/153-shadcn-line-dots-colors/case.json" ], "work": [ @@ -4401,17 +4467,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-dots-colors variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-dots-colors variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/153-shadcn-line-dots-colors/example.tsx", + "benchmarks/conformance/cases/153-shadcn-line-dots-colors/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4423,7 +4492,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/154-shadcn-line-dots-custom/tanstack.ts", + "cases/154-shadcn-line-dots-custom/example.tsx", "cases/154-shadcn-line-dots-custom/case.json" ], "work": [ @@ -4433,17 +4502,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-dots-custom variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-dots-custom variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/154-shadcn-line-dots-custom/example.tsx", + "benchmarks/conformance/cases/154-shadcn-line-dots-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4455,7 +4527,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/155-shadcn-line-dots/tanstack.ts", + "cases/155-shadcn-line-dots/example.tsx", "cases/155-shadcn-line-dots/case.json" ], "work": [ @@ -4465,17 +4537,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-dots variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-dots variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/155-shadcn-line-dots/example.tsx", + "benchmarks/conformance/cases/155-shadcn-line-dots/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4487,7 +4562,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/156-shadcn-line-interactive/tanstack.ts", + "cases/156-shadcn-line-interactive/example.tsx", "cases/156-shadcn-line-interactive/case.json" ], "work": [ @@ -4497,17 +4572,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-interactive variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-interactive variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/156-shadcn-line-interactive/example.tsx", + "benchmarks/conformance/cases/156-shadcn-line-interactive/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4519,7 +4597,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/157-shadcn-line-label-custom/tanstack.ts", + "cases/157-shadcn-line-label-custom/example.tsx", "cases/157-shadcn-line-label-custom/case.json" ], "work": [ @@ -4529,17 +4607,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-label-custom variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-label-custom variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/157-shadcn-line-label-custom/example.tsx", + "benchmarks/conformance/cases/157-shadcn-line-label-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4551,7 +4632,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/158-shadcn-line-label/tanstack.ts", + "cases/158-shadcn-line-label/example.tsx", "cases/158-shadcn-line-label/case.json" ], "work": [ @@ -4561,17 +4642,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/158-shadcn-line-label/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-label variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-label variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/158-shadcn-line-label/example.tsx", + "benchmarks/conformance/cases/158-shadcn-line-label/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4583,7 +4667,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/159-shadcn-line-linear/tanstack.ts", + "cases/159-shadcn-line-linear/example.tsx", "cases/159-shadcn-line-linear/case.json" ], "work": [ @@ -4593,17 +4677,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-linear variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-linear variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/159-shadcn-line-linear/example.tsx", + "benchmarks/conformance/cases/159-shadcn-line-linear/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4615,7 +4702,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/160-shadcn-line-multiple/tanstack.ts", + "cases/160-shadcn-line-multiple/example.tsx", "cases/160-shadcn-line-multiple/case.json" ], "work": [ @@ -4625,17 +4712,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-multiple variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-multiple variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/160-shadcn-line-multiple/example.tsx", + "benchmarks/conformance/cases/160-shadcn-line-multiple/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4647,7 +4737,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/161-shadcn-line-step/tanstack.ts", + "cases/161-shadcn-line-step/example.tsx", "cases/161-shadcn-line-step/case.json" ], "work": [ @@ -4657,17 +4747,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/161-shadcn-line-step/example.tsx" ], - "summary": "Compose the pinned shadcn chart-line-step variant through the shared native line chart definition." + "summary": "Define the pinned shadcn chart-line-step variant as a self-contained native line chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/161-shadcn-line-step/example.tsx", + "benchmarks/conformance/cases/161-shadcn-line-step/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4679,7 +4772,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/162-shadcn-pie-donut-active/tanstack.ts", + "cases/162-shadcn-pie-donut-active/example.tsx", "cases/162-shadcn-pie-donut-active/case.json" ], "work": [ @@ -4689,17 +4782,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-donut-active variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-donut-active variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/162-shadcn-pie-donut-active/example.tsx", + "benchmarks/conformance/cases/162-shadcn-pie-donut-active/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4711,7 +4807,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/129-shadcn-pie-donut-text/tanstack.ts", + "cases/129-shadcn-pie-donut-text/example.tsx", "cases/129-shadcn-pie-donut-text/case.json" ], "work": [ @@ -4721,17 +4817,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-donut-text variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-donut-text variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/129-shadcn-pie-donut-text/example.tsx", + "benchmarks/conformance/cases/129-shadcn-pie-donut-text/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4743,7 +4842,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/163-shadcn-pie-donut/tanstack.ts", + "cases/163-shadcn-pie-donut/example.tsx", "cases/163-shadcn-pie-donut/case.json" ], "work": [ @@ -4753,17 +4852,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-donut variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-donut variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/163-shadcn-pie-donut/example.tsx", + "benchmarks/conformance/cases/163-shadcn-pie-donut/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4775,7 +4877,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/164-shadcn-pie-interactive/tanstack.ts", + "cases/164-shadcn-pie-interactive/example.tsx", "cases/164-shadcn-pie-interactive/case.json" ], "work": [ @@ -4785,17 +4887,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-interactive variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-interactive variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/164-shadcn-pie-interactive/example.tsx", + "benchmarks/conformance/cases/164-shadcn-pie-interactive/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4807,7 +4912,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/165-shadcn-pie-label-custom/tanstack.ts", + "cases/165-shadcn-pie-label-custom/example.tsx", "cases/165-shadcn-pie-label-custom/case.json" ], "work": [ @@ -4817,17 +4922,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-label-custom variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-label-custom variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/165-shadcn-pie-label-custom/example.tsx", + "benchmarks/conformance/cases/165-shadcn-pie-label-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4839,7 +4947,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/166-shadcn-pie-label-list/tanstack.ts", + "cases/166-shadcn-pie-label-list/example.tsx", "cases/166-shadcn-pie-label-list/case.json" ], "work": [ @@ -4849,17 +4957,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-label-list variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-label-list variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/166-shadcn-pie-label-list/example.tsx", + "benchmarks/conformance/cases/166-shadcn-pie-label-list/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4871,7 +4982,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/167-shadcn-pie-label/tanstack.ts", + "cases/167-shadcn-pie-label/example.tsx", "cases/167-shadcn-pie-label/case.json" ], "work": [ @@ -4881,17 +4992,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-label variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-label variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/167-shadcn-pie-label/example.tsx", + "benchmarks/conformance/cases/167-shadcn-pie-label/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4903,7 +5017,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/168-shadcn-pie-legend/tanstack.ts", + "cases/168-shadcn-pie-legend/example.tsx", "cases/168-shadcn-pie-legend/case.json" ], "work": [ @@ -4913,17 +5027,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-legend variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-legend variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/168-shadcn-pie-legend/example.tsx", + "benchmarks/conformance/cases/168-shadcn-pie-legend/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4935,7 +5052,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/169-shadcn-pie-separator-none/tanstack.ts", + "cases/169-shadcn-pie-separator-none/example.tsx", "cases/169-shadcn-pie-separator-none/case.json" ], "work": [ @@ -4945,17 +5062,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-separator-none variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-separator-none variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/169-shadcn-pie-separator-none/example.tsx", + "benchmarks/conformance/cases/169-shadcn-pie-separator-none/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4967,7 +5087,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/170-shadcn-pie-simple/tanstack.ts", + "cases/170-shadcn-pie-simple/example.tsx", "cases/170-shadcn-pie-simple/case.json" ], "work": [ @@ -4977,17 +5097,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-simple variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-simple variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/170-shadcn-pie-simple/example.tsx", + "benchmarks/conformance/cases/170-shadcn-pie-simple/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -4999,7 +5122,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/171-shadcn-pie-stacked/tanstack.ts", + "cases/171-shadcn-pie-stacked/example.tsx", "cases/171-shadcn-pie-stacked/case.json" ], "work": [ @@ -5009,17 +5132,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx" ], - "summary": "Compose the pinned shadcn chart-pie-stacked variant through the shared native pie chart definition." + "summary": "Define the pinned shadcn chart-pie-stacked variant as a self-contained native pie chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/171-shadcn-pie-stacked/example.tsx", + "benchmarks/conformance/cases/171-shadcn-pie-stacked/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5031,7 +5157,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/172-shadcn-radar-default/tanstack.ts", + "cases/172-shadcn-radar-default/example.tsx", "cases/172-shadcn-radar-default/case.json" ], "work": [ @@ -5041,17 +5167,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-default variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-default variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/172-shadcn-radar-default/example.tsx", + "benchmarks/conformance/cases/172-shadcn-radar-default/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5063,7 +5192,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/173-shadcn-radar-dots/tanstack.ts", + "cases/173-shadcn-radar-dots/example.tsx", "cases/173-shadcn-radar-dots/case.json" ], "work": [ @@ -5073,17 +5202,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-dots variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-dots variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/173-shadcn-radar-dots/example.tsx", + "benchmarks/conformance/cases/173-shadcn-radar-dots/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5095,7 +5227,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/174-shadcn-radar-grid-circle-fill/tanstack.ts", + "cases/174-shadcn-radar-grid-circle-fill/example.tsx", "cases/174-shadcn-radar-grid-circle-fill/case.json" ], "work": [ @@ -5105,17 +5237,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-circle-fill variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-circle-fill variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/example.tsx", + "benchmarks/conformance/cases/174-shadcn-radar-grid-circle-fill/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5127,7 +5262,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/175-shadcn-radar-grid-circle-no-lines/tanstack.ts", + "cases/175-shadcn-radar-grid-circle-no-lines/example.tsx", "cases/175-shadcn-radar-grid-circle-no-lines/case.json" ], "work": [ @@ -5137,17 +5272,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-circle-no-lines variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-circle-no-lines variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/example.tsx", + "benchmarks/conformance/cases/175-shadcn-radar-grid-circle-no-lines/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5159,7 +5297,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/176-shadcn-radar-grid-circle/tanstack.ts", + "cases/176-shadcn-radar-grid-circle/example.tsx", "cases/176-shadcn-radar-grid-circle/case.json" ], "work": [ @@ -5169,17 +5307,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-circle variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-circle variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/example.tsx", + "benchmarks/conformance/cases/176-shadcn-radar-grid-circle/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5191,7 +5332,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/177-shadcn-radar-grid-custom/tanstack.ts", + "cases/177-shadcn-radar-grid-custom/example.tsx", "cases/177-shadcn-radar-grid-custom/case.json" ], "work": [ @@ -5201,17 +5342,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-custom variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-custom variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/example.tsx", + "benchmarks/conformance/cases/177-shadcn-radar-grid-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5223,7 +5367,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/178-shadcn-radar-grid-fill/tanstack.ts", + "cases/178-shadcn-radar-grid-fill/example.tsx", "cases/178-shadcn-radar-grid-fill/case.json" ], "work": [ @@ -5233,17 +5377,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-fill variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-fill variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/example.tsx", + "benchmarks/conformance/cases/178-shadcn-radar-grid-fill/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5255,7 +5402,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/179-shadcn-radar-grid-none/tanstack.ts", + "cases/179-shadcn-radar-grid-none/example.tsx", "cases/179-shadcn-radar-grid-none/case.json" ], "work": [ @@ -5265,17 +5412,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-grid-none variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-grid-none variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/179-shadcn-radar-grid-none/example.tsx", + "benchmarks/conformance/cases/179-shadcn-radar-grid-none/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5287,7 +5437,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/180-shadcn-radar-icons/tanstack.ts", + "cases/180-shadcn-radar-icons/example.tsx", "cases/180-shadcn-radar-icons/case.json" ], "work": [ @@ -5297,17 +5447,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-icons variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-icons variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/180-shadcn-radar-icons/example.tsx", + "benchmarks/conformance/cases/180-shadcn-radar-icons/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5319,7 +5472,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/181-shadcn-radar-label-custom/tanstack.ts", + "cases/181-shadcn-radar-label-custom/example.tsx", "cases/181-shadcn-radar-label-custom/case.json" ], "work": [ @@ -5329,17 +5482,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-label-custom variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-label-custom variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/181-shadcn-radar-label-custom/example.tsx", + "benchmarks/conformance/cases/181-shadcn-radar-label-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5351,7 +5507,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/182-shadcn-radar-legend/tanstack.ts", + "cases/182-shadcn-radar-legend/example.tsx", "cases/182-shadcn-radar-legend/case.json" ], "work": [ @@ -5361,17 +5517,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-legend variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-legend variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/182-shadcn-radar-legend/example.tsx", + "benchmarks/conformance/cases/182-shadcn-radar-legend/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5383,7 +5542,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/183-shadcn-radar-lines-only/tanstack.ts", + "cases/183-shadcn-radar-lines-only/example.tsx", "cases/183-shadcn-radar-lines-only/case.json" ], "work": [ @@ -5393,17 +5552,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-lines-only variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-lines-only variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/183-shadcn-radar-lines-only/example.tsx", + "benchmarks/conformance/cases/183-shadcn-radar-lines-only/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5415,7 +5577,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/130-shadcn-radar-multiple/tanstack.ts", + "cases/130-shadcn-radar-multiple/example.tsx", "cases/130-shadcn-radar-multiple/case.json" ], "work": [ @@ -5425,17 +5587,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-multiple variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-multiple variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/130-shadcn-radar-multiple/example.tsx", + "benchmarks/conformance/cases/130-shadcn-radar-multiple/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5447,7 +5612,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/184-shadcn-radar-radius/tanstack.ts", + "cases/184-shadcn-radar-radius/example.tsx", "cases/184-shadcn-radar-radius/case.json" ], "work": [ @@ -5457,17 +5622,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radar-radius variant through the shared native radar chart definition." + "summary": "Define the pinned shadcn chart-radar-radius variant as a self-contained native radar chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/184-shadcn-radar-radius/example.tsx", + "benchmarks/conformance/cases/184-shadcn-radar-radius/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5479,7 +5647,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/185-shadcn-radial-grid/tanstack.ts", + "cases/185-shadcn-radial-grid/example.tsx", "cases/185-shadcn-radial-grid/case.json" ], "work": [ @@ -5489,17 +5657,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-grid variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-grid variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/185-shadcn-radial-grid/example.tsx", + "benchmarks/conformance/cases/185-shadcn-radial-grid/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5511,7 +5682,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/186-shadcn-radial-label/tanstack.ts", + "cases/186-shadcn-radial-label/example.tsx", "cases/186-shadcn-radial-label/case.json" ], "work": [ @@ -5521,17 +5692,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-label variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-label variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/186-shadcn-radial-label/example.tsx", + "benchmarks/conformance/cases/186-shadcn-radial-label/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5543,7 +5717,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/187-shadcn-radial-shape/tanstack.ts", + "cases/187-shadcn-radial-shape/example.tsx", "cases/187-shadcn-radial-shape/case.json" ], "work": [ @@ -5553,17 +5727,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-shape variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-shape variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/187-shadcn-radial-shape/example.tsx", + "benchmarks/conformance/cases/187-shadcn-radial-shape/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5575,7 +5752,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/188-shadcn-radial-simple/tanstack.ts", + "cases/188-shadcn-radial-simple/example.tsx", "cases/188-shadcn-radial-simple/case.json" ], "work": [ @@ -5585,17 +5762,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-simple variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-simple variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/188-shadcn-radial-simple/example.tsx", + "benchmarks/conformance/cases/188-shadcn-radial-simple/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5607,7 +5787,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/189-shadcn-radial-stacked/tanstack.ts", + "cases/189-shadcn-radial-stacked/example.tsx", "cases/189-shadcn-radial-stacked/case.json" ], "work": [ @@ -5617,17 +5797,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-stacked variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-stacked variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/189-shadcn-radial-stacked/example.tsx", + "benchmarks/conformance/cases/189-shadcn-radial-stacked/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5639,7 +5822,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/131-shadcn-radial-text/tanstack.ts", + "cases/131-shadcn-radial-text/example.tsx", "cases/131-shadcn-radial-text/case.json" ], "work": [ @@ -5649,17 +5832,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx" ], - "summary": "Compose the pinned shadcn chart-radial-text variant through the shared native radial chart definition." + "summary": "Define the pinned shadcn chart-radial-text variant as a self-contained native radial chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/131-shadcn-radial-text/example.tsx", + "benchmarks/conformance/cases/131-shadcn-radial-text/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5671,7 +5857,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/132-shadcn-tooltip-advanced/tanstack.ts", + "cases/132-shadcn-tooltip-advanced/example.tsx", "cases/132-shadcn-tooltip-advanced/case.json" ], "work": [ @@ -5681,17 +5867,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-advanced variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-advanced variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/example.tsx", + "benchmarks/conformance/cases/132-shadcn-tooltip-advanced/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5703,7 +5892,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/190-shadcn-tooltip-default/tanstack.ts", + "cases/190-shadcn-tooltip-default/example.tsx", "cases/190-shadcn-tooltip-default/case.json" ], "work": [ @@ -5713,17 +5902,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-default variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-default variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/190-shadcn-tooltip-default/example.tsx", + "benchmarks/conformance/cases/190-shadcn-tooltip-default/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5735,7 +5927,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/191-shadcn-tooltip-formatter/tanstack.ts", + "cases/191-shadcn-tooltip-formatter/example.tsx", "cases/191-shadcn-tooltip-formatter/case.json" ], "work": [ @@ -5745,17 +5937,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-formatter variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-formatter variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/example.tsx", + "benchmarks/conformance/cases/191-shadcn-tooltip-formatter/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5767,7 +5962,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/192-shadcn-tooltip-icons/tanstack.ts", + "cases/192-shadcn-tooltip-icons/example.tsx", "cases/192-shadcn-tooltip-icons/case.json" ], "work": [ @@ -5777,17 +5972,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-icons variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-icons variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/192-shadcn-tooltip-icons/example.tsx", + "benchmarks/conformance/cases/192-shadcn-tooltip-icons/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5799,7 +5997,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/193-shadcn-tooltip-indicator-line/tanstack.ts", + "cases/193-shadcn-tooltip-indicator-line/example.tsx", "cases/193-shadcn-tooltip-indicator-line/case.json" ], "work": [ @@ -5809,17 +6007,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-indicator-line variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-indicator-line variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/example.tsx", + "benchmarks/conformance/cases/193-shadcn-tooltip-indicator-line/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5831,7 +6032,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/194-shadcn-tooltip-indicator-none/tanstack.ts", + "cases/194-shadcn-tooltip-indicator-none/example.tsx", "cases/194-shadcn-tooltip-indicator-none/case.json" ], "work": [ @@ -5841,17 +6042,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-indicator-none variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-indicator-none variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/example.tsx", + "benchmarks/conformance/cases/194-shadcn-tooltip-indicator-none/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5863,7 +6067,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/195-shadcn-tooltip-label-custom/tanstack.ts", + "cases/195-shadcn-tooltip-label-custom/example.tsx", "cases/195-shadcn-tooltip-label-custom/case.json" ], "work": [ @@ -5873,17 +6077,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-label-custom variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-label-custom variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/example.tsx", + "benchmarks/conformance/cases/195-shadcn-tooltip-label-custom/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5895,7 +6102,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/196-shadcn-tooltip-label-formatter/tanstack.ts", + "cases/196-shadcn-tooltip-label-formatter/example.tsx", "cases/196-shadcn-tooltip-label-formatter/case.json" ], "work": [ @@ -5905,17 +6112,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-label-formatter variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-label-formatter variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/example.tsx", + "benchmarks/conformance/cases/196-shadcn-tooltip-label-formatter/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5927,7 +6137,7 @@ "status": "verified", "capabilities": ["current-definition-api"], "evidence": [ - "cases/197-shadcn-tooltip-label-none/tanstack.ts", + "cases/197-shadcn-tooltip-label-none/example.tsx", "cases/197-shadcn-tooltip-label-none/case.json" ], "work": [ @@ -5937,17 +6147,20 @@ "owner": "charts", "coordinateSpace": "none", "sources": [ - "benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx" + "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx" ], - "summary": "Compose the pinned shadcn chart-tooltip-label-none variant through the shared native tooltip chart definition." + "summary": "Define the pinned shadcn chart-tooltip-label-none variant as a self-contained native tooltip chart example." }, { "kind": "application-shell", "stage": "post-render", "owner": "application", "coordinateSpace": "dom", - "sources": ["benchmarks/conformance/shared/shadcn-chart-card.tsx"], - "summary": "Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations." + "sources": [ + "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/example.tsx", + "benchmarks/conformance/cases/197-shadcn-tooltip-label-none/styles.css" + ], + "summary": "Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example." } ] }, @@ -5958,14 +6171,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/bar-grouped/tanstack.ts"], + "evidence": ["cases/bar-grouped/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public grouped reduction and grouped bar layout inside the responsive definition; width only controls tick-label presentation." } ] @@ -5977,14 +6190,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/bar-horizontal-ranking/tanstack.ts"], + "evidence": ["cases/bar-horizontal-ranking/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Keep authored row ordering beside native horizontal bars, baseline rule, band scale, and automatic label measurement." } ] @@ -5996,14 +6209,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/bar-stacked/tanstack.ts"], + "evidence": ["cases/bar-stacked/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose public fold and stack transforms directly with native bars while retaining authored cause order and colors." } ] @@ -6015,14 +6228,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/bar-vertical-sorted/tanstack.ts"], + "evidence": ["cases/bar-vertical-sorted/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Feed sorted category rows directly to native vertical bars; responsive definition input only rotates crowded tick labels." } ] @@ -6034,14 +6247,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/facets-anscombe/tanstack.ts"], + "evidence": ["cases/facets-anscombe/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use the native facet mark to create four child definitions with shared authored domains and source-bound dots." } ] @@ -6053,14 +6266,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/heatmap-labeled/tanstack.ts"], + "evidence": ["cases/heatmap-labeled/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Compose native cells and text over explicit category domains, including authored missing-value and contrast policies." } ] @@ -6072,14 +6285,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/histogram/tanstack.ts"], + "evidence": ["cases/histogram/example.tsx"], "work": [ { "kind": "data-transform", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "data", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Use public binX output directly as semantic rect intervals with native scales and focus lineage." } ] @@ -6091,14 +6304,14 @@ "phase": "phase-0", "status": "verified", "capabilities": ["current-definition-api"], - "evidence": ["cases/scatter-bubble/tanstack.ts"], + "evidence": ["cases/scatter-bubble/example.tsx"], "work": [ { "kind": "definition-composition", "stage": "definition-builder", "owner": "charts", "coordinateSpace": "none", - "sources": ["tanstack.ts"], + "sources": ["example.tsx"], "summary": "Feed model rows directly to a native dot mark with color and radius channels plus ordinary legends and scales." } ] diff --git a/benchmarks/conformance/definition-coverage-roadmap.test.ts b/benchmarks/conformance/definition-coverage-roadmap.test.ts index b3e9d91a..5f55b3d4 100644 --- a/benchmarks/conformance/definition-coverage-roadmap.test.ts +++ b/benchmarks/conformance/definition-coverage-roadmap.test.ts @@ -318,7 +318,7 @@ describe('definition coverage roadmap', () => { } else { expect( authoredClosure.has(source), - `${entry.id}: ${source} is not reachable from tanstack.ts`, + `${entry.id}: ${source} is not reachable from example.tsx`, ).toBe(true) } expect(existsSync(sourcePath)).toBe(true) diff --git a/benchmarks/conformance/native-catalog.ts b/benchmarks/conformance/native-catalog.ts index 7f84dbe3..d4040323 100644 --- a/benchmarks/conformance/native-catalog.ts +++ b/benchmarks/conformance/native-catalog.ts @@ -34,5 +34,5 @@ export function loadTanStackImplementation( } export function loadTanStackSources(id: string) { - return loadCatalogSourceClosure(sourceModules, `./cases/${id}/tanstack.ts`) + return loadCatalogSourceClosure(sourceModules, `./cases/${id}/example.tsx`) } diff --git a/benchmarks/conformance/previews/01-line-gaps.svg b/benchmarks/conformance/previews/01-line-gaps.svg index 16f3b85d..90553b6e 100644 --- a/benchmarks/conformance/previews/01-line-gaps.svg +++ b/benchmarks/conformance/previews/01-line-gaps.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/03-temperature-range-band.svg b/benchmarks/conformance/previews/03-temperature-range-band.svg index 729aa933..7b5ea178 100644 --- a/benchmarks/conformance/previews/03-temperature-range-band.svg +++ b/benchmarks/conformance/previews/03-temperature-range-band.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/04-stacked-time-area.svg b/benchmarks/conformance/previews/04-stacked-time-area.svg index b61fe382..46f39097 100644 --- a/benchmarks/conformance/previews/04-stacked-time-area.svg +++ b/benchmarks/conformance/previews/04-stacked-time-area.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/106-polar-line.svg b/benchmarks/conformance/previews/106-polar-line.svg index 55725ff5..b11a6a91 100644 --- a/benchmarks/conformance/previews/106-polar-line.svg +++ b/benchmarks/conformance/previews/106-polar-line.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/109-us-state-choropleth.svg b/benchmarks/conformance/previews/109-us-state-choropleth.svg index 13aee966..bbcce99a 100644 --- a/benchmarks/conformance/previews/109-us-state-choropleth.svg +++ b/benchmarks/conformance/previews/109-us-state-choropleth.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/110-projection-gallery.svg b/benchmarks/conformance/previews/110-projection-gallery.svg index b6e08026..efc13ec8 100644 --- a/benchmarks/conformance/previews/110-projection-gallery.svg +++ b/benchmarks/conformance/previews/110-projection-gallery.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg b/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg index b24672d0..0cbb5785 100644 --- a/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg +++ b/benchmarks/conformance/previews/128-shadcn-bar-multiple.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg b/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg index e165c936..56ae7dd0 100644 --- a/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg +++ b/benchmarks/conformance/previews/129-shadcn-pie-donut-text.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg b/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg index 613a72fd..672f5f85 100644 --- a/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg +++ b/benchmarks/conformance/previews/130-shadcn-radar-multiple.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/131-shadcn-radial-text.svg b/benchmarks/conformance/previews/131-shadcn-radial-text.svg index fd18711f..46c967ae 100644 --- a/benchmarks/conformance/previews/131-shadcn-radial-text.svg +++ b/benchmarks/conformance/previews/131-shadcn-radial-text.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg index a6f7c706..257e089a 100644 --- a/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg +++ b/benchmarks/conformance/previews/132-shadcn-tooltip-advanced.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/19-moving-average-line.svg b/benchmarks/conformance/previews/19-moving-average-line.svg index 00f3f1ae..7b636fe7 100644 --- a/benchmarks/conformance/previews/19-moving-average-line.svg +++ b/benchmarks/conformance/previews/19-moving-average-line.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/20-normalized-stacked-area.svg b/benchmarks/conformance/previews/20-normalized-stacked-area.svg index abe3f45d..97cf1b26 100644 --- a/benchmarks/conformance/previews/20-normalized-stacked-area.svg +++ b/benchmarks/conformance/previews/20-normalized-stacked-area.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/21-streamgraph.svg b/benchmarks/conformance/previews/21-streamgraph.svg index 64dd498a..77f0e79a 100644 --- a/benchmarks/conformance/previews/21-streamgraph.svg +++ b/benchmarks/conformance/previews/21-streamgraph.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/22-bollinger-band.svg b/benchmarks/conformance/previews/22-bollinger-band.svg index 1648705c..a2eb8da1 100644 --- a/benchmarks/conformance/previews/22-bollinger-band.svg +++ b/benchmarks/conformance/previews/22-bollinger-band.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/31-linear-regression.svg b/benchmarks/conformance/previews/31-linear-regression.svg index 2076b5b3..db5e1baf 100644 --- a/benchmarks/conformance/previews/31-linear-regression.svg +++ b/benchmarks/conformance/previews/31-linear-regression.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/44-framed-scatter.svg b/benchmarks/conformance/previews/44-framed-scatter.svg index a7eb7339..be802a88 100644 --- a/benchmarks/conformance/previews/44-framed-scatter.svg +++ b/benchmarks/conformance/previews/44-framed-scatter.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/50-empirical-cdf.svg b/benchmarks/conformance/previews/50-empirical-cdf.svg index 7fb5bc18..b05fe90e 100644 --- a/benchmarks/conformance/previews/50-empirical-cdf.svg +++ b/benchmarks/conformance/previews/50-empirical-cdf.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/53-log-scale-scatter.svg b/benchmarks/conformance/previews/53-log-scale-scatter.svg index 895f9b80..cc5917df 100644 --- a/benchmarks/conformance/previews/53-log-scale-scatter.svg +++ b/benchmarks/conformance/previews/53-log-scale-scatter.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg b/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg index 84b134b6..7a5ee0cc 100644 --- a/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg +++ b/benchmarks/conformance/previews/57-scatter-marginal-histograms.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/58-select-extrema.svg b/benchmarks/conformance/previews/58-select-extrema.svg index 00af0100..d6a1f998 100644 --- a/benchmarks/conformance/previews/58-select-extrema.svg +++ b/benchmarks/conformance/previews/58-select-extrema.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/61-quantile-ribbon.svg b/benchmarks/conformance/previews/61-quantile-ribbon.svg index a4dbb92a..60138a0e 100644 --- a/benchmarks/conformance/previews/61-quantile-ribbon.svg +++ b/benchmarks/conformance/previews/61-quantile-ribbon.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/73-many-point-scatter.svg b/benchmarks/conformance/previews/73-many-point-scatter.svg index 4db18543..0afb1fa1 100644 --- a/benchmarks/conformance/previews/73-many-point-scatter.svg +++ b/benchmarks/conformance/previews/73-many-point-scatter.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/81-recharts-interactive-legend.svg b/benchmarks/conformance/previews/81-recharts-interactive-legend.svg index 09ac2da3..a081b284 100644 --- a/benchmarks/conformance/previews/81-recharts-interactive-legend.svg +++ b/benchmarks/conformance/previews/81-recharts-interactive-legend.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json index 911e6f0d..a025bfaf 100644 --- a/benchmarks/conformance/previews/manifest.json +++ b/benchmarks/conformance/previews/manifest.json @@ -2,12 +2,12 @@ "schemaVersion": 1, "width": 288, "height": 192, - "sourceHash": "535c55f7795666a08afa8f11e89bf8214ecece08bdbd89aa87423ac4d33f0713", + "sourceHash": "f6e45af3af0d44a468e0a33b1bda18065ae1fbd1e3a675778f6586ef426fcb9c", "assets": [ { "id": "01-line-gaps", - "sha256": "51680de1ad7c16ad1ede898ff973c49213597661bc994f53b8f1476b12372cd6", - "bytes": 2987 + "sha256": "2477366741cb7397c34b3741e845b997201c4f61a513c40a95c616af126cf8fe", + "bytes": 14606 }, { "id": "02-multi-line-end-labels", @@ -16,13 +16,13 @@ }, { "id": "03-temperature-range-band", - "sha256": "10af9b9796b9d8d131e2cf216f9f773b2c9879b91c0e0fe85666ea9aacda5694", - "bytes": 5707 + "sha256": "799a823d73454dd5732079439849a1c0e806b87096e6950992510612d2711213", + "bytes": 39637 }, { "id": "04-stacked-time-area", - "sha256": "2526abafdadaf70a3e137fd11240bc0b1db8a866f3b4d9861552c5d45cc60e4d", - "bytes": 14945 + "sha256": "f5d3df0a359c174a041ec7e804c10ac7d3e41c76215ce3e9fc2f6a58e54f5b6d", + "bytes": 47969 }, { "id": "bar-vertical-sorted", @@ -46,8 +46,8 @@ }, { "id": "scatter-bubble", - "sha256": "3ddd8931616063fc3b8d852ac76f70571f0a6f7b93b8c2c75ba14c66ff0c721d", - "bytes": 18082 + "sha256": "8b076b18b3fd3f3c1f383c8ac033034e9974eccbf9c2da7a92ec2fe1f719b8fb", + "bytes": 71731 }, { "id": "histogram", @@ -96,23 +96,23 @@ }, { "id": "19-moving-average-line", - "sha256": "1e9194565783f5409f5fd07d45c75fd4f072ef0ae51dc342dcb31669d4a07c51", - "bytes": 3720 + "sha256": "d34ece29ea71d55caed5aa92f086abec7306e8eaf0b04311c3d09109c505b856", + "bytes": 19928 }, { "id": "20-normalized-stacked-area", - "sha256": "1b69d2a7545e3f093e9ac964ad466a79892ef9edea1e678d18c059806f6ee7d1", - "bytes": 14116 + "sha256": "9285d573b528aa39375dfc2a3f5813f861c9aba535bbdea53993d8d57e573f96", + "bytes": 44882 }, { "id": "21-streamgraph", - "sha256": "a05828d714e187216366855052791f27e4d1b03d44b0d545dd47c6508a9ba658", - "bytes": 11480 + "sha256": "346102106ccc8368da981218fcf909ebd9d62a0e13ccf695e0d0db2fff51dbac", + "bytes": 46869 }, { "id": "22-bollinger-band", - "sha256": "b7662fee3ddceb31b5acdc6fff72c12e53ec3c6e2d1428d204cd70dafc8779b7", - "bytes": 4511 + "sha256": "89ca449a357519b19506ee43ca555e32580d97d529448eb0c1c6ed94ab0c0583", + "bytes": 49965 }, { "id": "24-quantitative-binned-heatmap", @@ -151,8 +151,8 @@ }, { "id": "31-linear-regression", - "sha256": "76c382ec09b0a0aa80f2131db69e19a3c1923155968f9fbc0ee4f66892af199c", - "bytes": 14155 + "sha256": "bf968cd8bae74c796c25b976d6f908e50534170a91ca19bdc731b57b85efc7a5", + "bytes": 50604 }, { "id": "32-change-arrows", @@ -221,13 +221,13 @@ }, { "id": "44-framed-scatter", - "sha256": "c5805f843b23b44344921e561ba0ad5b00c0bff45e5c5be656e4c815a912a49c", - "bytes": 12730 + "sha256": "473c0f93bcdb6a4103a745989b09e2b06119a51ea291b6226f2b35ae2cb07b09", + "bytes": 47444 }, { "id": "50-empirical-cdf", - "sha256": "f988a646a6672a30f7ac3e15e360b1b57fce9396299515edf5804a9c735d73cc", - "bytes": 3544 + "sha256": "3e51a0b588231ee1e0a0ff3c66a8cc2491f6fc3f70bc968fcb510219bf85f268", + "bytes": 13060 }, { "id": "51-faceted-distributions", @@ -241,8 +241,8 @@ }, { "id": "53-log-scale-scatter", - "sha256": "96d63b6eb2cc293e4b8404442a0bf322a08dd9974aec24e3a5657c37b2762a5d", - "bytes": 14417 + "sha256": "f4318a8317fed7143bbe83728e3b2f4ab64cf474a40c67be1a390bd55df8a637", + "bytes": 35235 }, { "id": "54-bump-ranking", @@ -261,13 +261,13 @@ }, { "id": "57-scatter-marginal-histograms", - "sha256": "b0ede85fc11bda7cdaa0a8f690b357e98333a5f759020c73fc26b9950af013e0", - "bytes": 23643 + "sha256": "8dfcf922b5d4560fa0813b95df15edba3b41631055d54942bc697731ce42d803", + "bytes": 75618 }, { "id": "58-select-extrema", - "sha256": "7d5f0cad7846cd00abc67ba07e70cd9914f362d659691501ccf115776d654b20", - "bytes": 3232 + "sha256": "6fb8771d26d5cb717fb5786df2efffe1b022b8a224866828dabdbfbd47dd57fe", + "bytes": 18569 }, { "id": "59-grouped-reducer-bars", @@ -281,8 +281,8 @@ }, { "id": "61-quantile-ribbon", - "sha256": "b0b30325ad2af7ea91c601674647b7b375a36a2863e53a91d3a6373adcd7bac3", - "bytes": 3894 + "sha256": "dc7d68dce0c68ae65ab282b51e69f5cbff4364b5c24f7d1f1b82f3984b0b3822", + "bytes": 6360 }, { "id": "62-ridgeline-density", @@ -321,8 +321,8 @@ }, { "id": "73-many-point-scatter", - "sha256": "3be3f80014992552da83e4cc6ef59572995ec400aefb0015001968d373d5b9f7", - "bytes": 13551 + "sha256": "ae2f11540ff7494e56023c1b4b4967861679dbc40a0070531a83de2e72a0402b", + "bytes": 47822 }, { "id": "74-recharts-treemap", @@ -356,8 +356,8 @@ }, { "id": "81-recharts-interactive-legend", - "sha256": "2f456f50772dc7e0118d277f97a155edc5988f9dee183c6a752d9e664c18ab55", - "bytes": 5131 + "sha256": "04fff0cd593575a93d5ca67f8d7695e281678de6abf90e6d142ebd9b7862195e", + "bytes": 5138 }, { "id": "82-chart-table-selection", @@ -481,8 +481,8 @@ }, { "id": "106-polar-line", - "sha256": "ef6f3971a3efe151f2501e32e8203f24bcee40a814b15e6855946e0cac779c7b", - "bytes": 4844 + "sha256": "d2a78e38ea6b020700252d8b1ebfe0b7ac3627ab228be727c8f5d56e39124a5a", + "bytes": 8960 }, { "id": "107-polar-scatter", @@ -496,13 +496,13 @@ }, { "id": "109-us-state-choropleth", - "sha256": "5a621e61cfd1bc20a785f973249a71696156f873df89484b295b4bcdca2831c5", - "bytes": 797953 + "sha256": "bb6e775a8e55eb545ca463737c663ebab621f9774dbd28e010b93df7aaf00c01", + "bytes": 1478794 }, { "id": "110-projection-gallery", - "sha256": "e036cc78ad4dec74a13068cb5cf114fe8455932f562f4ebeab3de8cbcd1053ef", - "bytes": 71929 + "sha256": "5192a1d1e6584186e5ee9d6d9b7066e4dfe40e00039e8dac2e5eee1880305c86", + "bytes": 287966 }, { "id": "111-basic-sankey", @@ -596,28 +596,28 @@ }, { "id": "128-shadcn-bar-multiple", - "sha256": "fdde89d276f842819c0d7b4ec8b29177f45bc31b44b530f712e7bbde86d9ef09", - "bytes": 3621 + "sha256": "f79c2f4b0c4af3e682eec9dc5753907d4763da9df5f5a01ac36f49cbe24260a4", + "bytes": 5214 }, { "id": "129-shadcn-pie-donut-text", - "sha256": "cea4adc3f89d3008744f5ec9e6513cc6d713c284c81e65aee974d8896b3dc66a", - "bytes": 3452 + "sha256": "f0b04339348b304ee22b33092827b57053a06cb8e24fe547e15e09168a593ce5", + "bytes": 3336 }, { "id": "130-shadcn-radar-multiple", - "sha256": "21c4c3bdcb494b53bfc32722c8d34a0ad708bb9fef266cfafa619d6c4e7f1502", - "bytes": 5245 + "sha256": "cba2e60d18646c5836ca9933b85d50b314cd0462c81a746e87097b42db45e702", + "bytes": 5393 }, { "id": "131-shadcn-radial-text", - "sha256": "a5152ef6e25530cb6b2aab4270ff27183b12603d47a81735c153c30da8aee3eb", - "bytes": 2796 + "sha256": "b07be200fe2b9e5b00726f57407a6e91a8fdb8dc11d149bfad54e5a1ebac2c28", + "bytes": 2780 }, { "id": "132-shadcn-tooltip-advanced", - "sha256": "b7d813f5d1514872c2af10cdfccf2de4314f685b996de3b0e3e13295ff3fc61c", - "bytes": 3699 + "sha256": "98aaed6671a1005c2c0eef880633cdbfbf27a4717bc5b996960a601562065df0", + "bytes": 4865 }, { "id": "133-shadcn-area-axes", diff --git a/benchmarks/conformance/previews/scatter-bubble.svg b/benchmarks/conformance/previews/scatter-bubble.svg index 68ac7e4c..32fb9601 100644 --- a/benchmarks/conformance/previews/scatter-bubble.svg +++ b/benchmarks/conformance/previews/scatter-bubble.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/shared/fixtures/country-atlas.ts b/benchmarks/conformance/shared/fixtures/country-atlas.ts index e11ea9af..78cc9c3b 100644 --- a/benchmarks/conformance/shared/fixtures/country-atlas.ts +++ b/benchmarks/conformance/shared/fixtures/country-atlas.ts @@ -1,135 +1 @@ -import countriesAtlasJson from 'world-atlas/countries-110m.json' -import landAtlasJson from 'world-atlas/land-110m.json' -import detailedLandAtlasJson from 'world-atlas/land-50m.json' -import { geoGraticule, geoGraticule10 } from 'd3-geo' -import { feature } from 'topojson-client' -import { simplifyPolygonGeometry } from './simplify-geo' -import type { - ExtendedFeature, - ExtendedFeatureCollection, - GeoGeometryObjects, - GeoSphere, -} from 'd3-geo' - -type AtlasTopology = Parameters[0] - -export type CountryGeometry = Extract< - GeoGeometryObjects, - { type: 'Polygon' | 'MultiPolygon' } -> - -export interface CountryProperties { - name: string -} - -export type CountryFeature = ExtendedFeature -export type LandFeature = ExtendedFeature> - -export const worldSphere: GeoSphere = { type: 'Sphere' } -export const worldGraticule = geoGraticule10() -export const previewWorldGraticule = geoGraticule().step([30, 30])() - -const countriesTopology = atlasTopology( - countriesAtlasJson, - 'world-atlas countries-110m', -) -const countriesObject = countriesTopology.objects.countries -if (!countriesObject) { - throw new TypeError('world-atlas countries-110m is missing countries') -} - -const convertedCountries = feature(countriesTopology, countriesObject) -if (convertedCountries.type !== 'FeatureCollection') { - throw new TypeError('world-atlas countries did not produce a collection') -} - -export const worldCountries: readonly CountryFeature[] = - convertedCountries.features.flatMap((entry) => { - if ( - !isCountryGeometry(entry.geometry) || - !isRecord(entry.properties) || - typeof entry.properties.name !== 'string' - ) { - return [] - } - - return [ - { - type: 'Feature', - id: entry.id === undefined ? entry.properties.name : String(entry.id), - geometry: entry.geometry, - properties: { - name: entry.properties.name, - }, - }, - ] - }) - -if (worldCountries.length !== 177) { - throw new TypeError( - `Expected 177 world-atlas countries, got ${worldCountries.length}`, - ) -} - -export const worldCountryCollection: ExtendedFeatureCollection = - { - type: 'FeatureCollection', - features: [...worldCountries], - } - -export const worldLand = convertLand(landAtlasJson, 'world-atlas land-110m') -export const previewWorldLand: LandFeature = { - ...worldLand, - geometry: simplifyPolygonGeometry(worldLand.geometry, 2), -} -export const detailedWorldLand = convertLand( - detailedLandAtlasJson, - 'world-atlas land-50m', -) - -function atlasTopology(value: unknown, label: string): AtlasTopology { - if (!isAtlasTopology(value)) { - throw new TypeError(`${label} is not valid TopoJSON`) - } - return value -} - -function convertLand(value: unknown, label: string): LandFeature { - const topology = atlasTopology(value, label) - const landObject = topology.objects.land - if (!landObject) { - throw new TypeError(`${label} is missing land`) - } - - const converted = feature(topology, landObject) - const land = - converted.type === 'FeatureCollection' ? converted.features[0] : converted - if (!land || land.type !== 'Feature' || !isCountryGeometry(land.geometry)) { - throw new TypeError(`${label} did not produce polygon geometry`) - } - - return { - type: 'Feature', - geometry: land.geometry, - properties: {}, - } -} - -function isCountryGeometry( - geometry: GeoGeometryObjects, -): geometry is CountryGeometry { - return geometry.type === 'Polygon' || geometry.type === 'MultiPolygon' -} - -function isAtlasTopology(value: unknown): value is AtlasTopology { - return ( - isRecord(value) && - value.type === 'Topology' && - Array.isArray(value.arcs) && - isRecord(value.objects) - ) -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null -} +export * from '@charts-poc/demo-data/country-atlas' diff --git a/benchmarks/conformance/shared/fixtures/geography.test.ts b/benchmarks/conformance/shared/fixtures/geography.test.ts index 18ad1152..2e424863 100644 --- a/benchmarks/conformance/shared/fixtures/geography.test.ts +++ b/benchmarks/conformance/shared/fixtures/geography.test.ts @@ -7,7 +7,11 @@ import { projectedUnemploymentCounties, unemploymentCounties, } from '../../cases/109-us-state-choropleth/transform' -import { detailedWorldLand, worldCountries, worldLand } from './country-atlas' +import { + detailedWorldLand, + worldCountries, + worldLand, +} from '@charts-poc/demo-data/country-atlas' import { learningPovertyCountries, learningPovertyPointsByPopulation, diff --git a/benchmarks/conformance/shared/fixtures/simplify-geo.ts b/benchmarks/conformance/shared/fixtures/simplify-geo.ts index 5486e5be..716d8069 100644 --- a/benchmarks/conformance/shared/fixtures/simplify-geo.ts +++ b/benchmarks/conformance/shared/fixtures/simplify-geo.ts @@ -1,130 +1 @@ -import type { GeoGeometryObjects } from 'd3-geo' - -type PolygonGeometry = Extract< - GeoGeometryObjects, - { type: 'Polygon' | 'MultiPolygon' } -> -type Position = number[] - -export function simplifyPolygonGeometry( - geometry: PolygonGeometry, - tolerance: number, -): PolygonGeometry { - if (geometry.type === 'Polygon') { - return { - type: 'Polygon', - coordinates: geometry.coordinates.map((ring) => - simplifyRing(ring, tolerance), - ), - } - } - - return { - type: 'MultiPolygon', - coordinates: geometry.coordinates.map((polygon) => - polygon.map((ring) => simplifyRing(ring, tolerance)), - ), - } -} - -function simplifyRing( - ring: readonly Position[], - tolerance: number, -): Position[] { - if (ring.length <= 4) return [...ring] - - const openRing = ring.slice(0, -1) - const anchor = openRing[0] - if (!anchor) return [...ring] - - let splitIndex = 1 - let farthestDistance = 0 - for (let index = 1; index < openRing.length; index += 1) { - const point = openRing[index] - if (!point) continue - const distance = squaredDistance(anchor, point) - if (distance > farthestDistance) { - farthestDistance = distance - splitIndex = index - } - } - - const firstHalf = simplifyLine( - openRing.slice(0, splitIndex + 1), - tolerance * tolerance, - ) - const secondHalf = simplifyLine( - [...openRing.slice(splitIndex), anchor], - tolerance * tolerance, - ) - const simplified = [...firstHalf.slice(0, -1), ...secondHalf] - - return simplified.length >= 4 ? simplified : [...ring] -} - -function simplifyLine( - points: readonly Position[], - squaredTolerance: number, -): Position[] { - const first = points[0] - const last = points.at(-1) - if (!first || !last || points.length <= 2) return [...points] - - let farthestIndex = 0 - let farthestDistance = squaredTolerance - for (let index = 1; index < points.length - 1; index += 1) { - const point = points[index] - if (!point) continue - const distance = squaredSegmentDistance(point, first, last) - if (distance > farthestDistance) { - farthestDistance = distance - farthestIndex = index - } - } - - if (farthestIndex === 0) return [first, last] - - const left = simplifyLine( - points.slice(0, farthestIndex + 1), - squaredTolerance, - ) - const right = simplifyLine(points.slice(farthestIndex), squaredTolerance) - return [...left.slice(0, -1), ...right] -} - -function squaredSegmentDistance( - point: Position, - start: Position, - end: Position, -): number { - const [pointX = 0, pointY = 0] = point - let [x = 0, y = 0] = start - const [endX = 0, endY = 0] = end - let dx = endX - x - let dy = endY - y - - if (dx !== 0 || dy !== 0) { - const progress = - ((pointX - x) * dx + (pointY - y) * dy) / (dx * dx + dy * dy) - if (progress > 1) { - x = endX - y = endY - } else if (progress > 0) { - x += dx * progress - y += dy * progress - } - dx = pointX - x - dy = pointY - y - } else { - dx = pointX - x - dy = pointY - y - } - - return dx * dx + dy * dy -} - -function squaredDistance(left: Position, right: Position): number { - const dx = (left[0] ?? 0) - (right[0] ?? 0) - const dy = (left[1] ?? 0) - (right[1] ?? 0) - return dx * dx + dy * dy -} +export * from '@charts-poc/demo-data/simplify-geo' diff --git a/benchmarks/conformance/shared/mount.ts b/benchmarks/conformance/shared/mount.ts index 05db1ca4..93f7af66 100644 --- a/benchmarks/conformance/shared/mount.ts +++ b/benchmarks/conformance/shared/mount.ts @@ -48,6 +48,45 @@ export function tanstackMount< ariaLabel: string, interactiveTooltip: true | ChartTooltipOptions = true, previewOptions: CatalogPreviewOptions = {}, +): TanStackConformanceCase { + return createTanstackMount( + createDefinition, + ariaLabel, + interactiveTooltip, + previewOptions, + ) +} + +export function tanstackExampleMount< + TDatum, + TXValue extends ChartValue = ChartValue, + TYValue extends ChartValue = ChartValue, +>( + createDefinition: ( + input: ConformanceInput, + ) => DomChartDefinition, + ariaLabel: string, + previewOptions: CatalogPreviewOptions = {}, +): TanStackConformanceCase { + return createTanstackMount( + createDefinition, + ariaLabel, + undefined, + previewOptions, + ) +} + +function createTanstackMount< + TDatum, + TXValue extends ChartValue = ChartValue, + TYValue extends ChartValue = ChartValue, +>( + createDefinition: ( + input: ConformanceInput, + ) => DomChartDefinition, + ariaLabel: string, + interactiveTooltip: true | ChartTooltipOptions | undefined, + previewOptions: CatalogPreviewOptions, ): TanStackConformanceCase { const mount: ConformanceMount = (container, input) => { const options = { @@ -104,7 +143,7 @@ export interface TanStackConformanceCase< input: ConformanceInput, ) => DomChartDefinition ariaLabel: string - interactiveTooltip: true | ChartTooltipOptions + interactiveTooltip: true | ChartTooltipOptions | undefined mount: ConformanceMount } @@ -135,7 +174,7 @@ export function withConformanceBehavior< >( definition: DomChartDefinition, input: ConformanceInput, - interactiveTooltip: true | ChartTooltipOptions, + interactiveTooltip: true | ChartTooltipOptions | undefined, previewOptions: CatalogPreviewOptions = {}, ): DomChartDefinition { const presentation = @@ -149,12 +188,16 @@ export function withConformanceBehavior< ? {} : { focus: false }), keyboard: input.interactive === true, - tooltip: - input.interactive !== true - ? false - : interactiveTooltip === true - ? tooltip - : { use: tooltip, ...interactiveTooltip }, + ...(input.interactive !== true + ? { tooltip: false as const } + : interactiveTooltip === undefined + ? {} + : { + tooltip: + interactiveTooltip === true + ? tooltip + : { use: tooltip, ...interactiveTooltip }, + }), } if (isResponsiveChartDefinition(presentation)) { diff --git a/benchmarks/conformance/shared/shadcn-catalog-data.ts b/benchmarks/conformance/shared/shadcn-catalog-data.ts index 5030c176..016035c5 100644 --- a/benchmarks/conformance/shared/shadcn-catalog-data.ts +++ b/benchmarks/conformance/shared/shadcn-catalog-data.ts @@ -1,216 +1 @@ -export type ShadcnChartFamily = - 'area' | 'bar' | 'line' | 'pie' | 'radar' | 'radial' | 'tooltip' - -export interface ShadcnCatalogSpec { - name: string - family: ShadcnChartFamily - variant: string - title: string - description: string - footerNote: string - square: boolean - legend: boolean -} - -export interface ShadcnMonthDatum { - month: string - desktop: number - mobile: number - tablet: number -} - -export interface ShadcnSeriesDatum { - month: string - series: 'desktop' | 'mobile' | 'tablet' | 'other' - value: number -} - -export interface ShadcnBrowserDatum { - browser: string - visitors: number -} - -export interface ShadcnRadarDatum { - month: string - desktop: number - mobile?: number -} - -export interface ShadcnActivityDatum { - date: string - activity: 'running' | 'swimming' - value: number -} - -export const shadcnMonths: readonly ShadcnMonthDatum[] = [ - { month: 'January', desktop: 186, mobile: 80, tablet: 44 }, - { month: 'February', desktop: 305, mobile: 200, tablet: 72 }, - { month: 'March', desktop: 237, mobile: 120, tablet: 58 }, - { month: 'April', desktop: 73, mobile: 190, tablet: 91 }, - { month: 'May', desktop: 209, mobile: 130, tablet: 67 }, - { month: 'June', desktop: 214, mobile: 140, tablet: 82 }, -] - -export const shadcnSeriesRows: readonly ShadcnSeriesDatum[] = - shadcnMonths.flatMap((row) => [ - { month: row.month, series: 'desktop', value: row.desktop }, - { month: row.month, series: 'mobile', value: row.mobile }, - { month: row.month, series: 'tablet', value: row.tablet }, - ]) - -export const shadcnBrowsers: readonly ShadcnBrowserDatum[] = [ - { browser: 'chrome', visitors: 275 }, - { browser: 'safari', visitors: 200 }, - { browser: 'firefox', visitors: 187 }, - { browser: 'edge', visitors: 173 }, - { browser: 'other', visitors: 90 }, -] - -export const shadcnRadarDefault: readonly ShadcnRadarDatum[] = [ - { month: 'January', desktop: 186 }, - { month: 'February', desktop: 305 }, - { month: 'March', desktop: 237 }, - { month: 'April', desktop: 273 }, - { month: 'May', desktop: 209 }, - { month: 'June', desktop: 214 }, -] - -export const shadcnRadarFilled: readonly ShadcnRadarDatum[] = [ - { month: 'January', desktop: 186 }, - { month: 'February', desktop: 285 }, - { month: 'March', desktop: 237 }, - { month: 'April', desktop: 203 }, - { month: 'May', desktop: 209 }, - { month: 'June', desktop: 264 }, -] - -export const shadcnRadarMultiple: readonly ShadcnRadarDatum[] = - shadcnMonths.map(({ month, desktop, mobile }) => ({ - month, - desktop, - mobile, - })) - -export const shadcnRadarLines: readonly ShadcnRadarDatum[] = [ - { month: 'January', desktop: 186, mobile: 160 }, - { month: 'February', desktop: 185, mobile: 170 }, - { month: 'March', desktop: 207, mobile: 180 }, - { month: 'April', desktop: 173, mobile: 160 }, - { month: 'May', desktop: 160, mobile: 190 }, - { month: 'June', desktop: 174, mobile: 204 }, -] - -export const shadcnActivities: readonly ShadcnActivityDatum[] = [ - { date: '2024-07-15', activity: 'running', value: 450 }, - { date: '2024-07-15', activity: 'swimming', value: 300 }, - { date: '2024-07-16', activity: 'running', value: 380 }, - { date: '2024-07-16', activity: 'swimming', value: 420 }, - { date: '2024-07-17', activity: 'running', value: 520 }, - { date: '2024-07-17', activity: 'swimming', value: 120 }, - { date: '2024-07-18', activity: 'running', value: 140 }, - { date: '2024-07-18', activity: 'swimming', value: 550 }, - { date: '2024-07-19', activity: 'running', value: 600 }, - { date: '2024-07-19', activity: 'swimming', value: 350 }, - { date: '2024-07-20', activity: 'running', value: 480 }, - { date: '2024-07-20', activity: 'swimming', value: 400 }, -] - -export const shadcnColors = [ - 'var(--chart-1, var(--ts-chart-1))', - 'var(--chart-2, var(--ts-chart-2))', - 'var(--chart-3, var(--ts-chart-3))', - 'var(--chart-4, var(--ts-chart-4))', - 'var(--chart-5, var(--ts-chart-5))', -] as const - -const titleOverrides: Record = { - 'chart-area-default': 'Area Chart', - 'chart-area-stacked-expand': 'Area Chart - Stacked Expanded', - 'chart-bar-default': 'Bar Chart', - 'chart-bar-label-custom': 'Bar Chart - Custom Label', - 'chart-bar-stacked': 'Bar Chart - Stacked + Legend', - 'chart-line-default': 'Line Chart', - 'chart-line-dots-custom': 'Line Chart - Custom Dots', - 'chart-line-label-custom': 'Line Chart - Custom Label', - 'chart-pie-simple': 'Pie Chart', - 'chart-pie-donut-text': 'Pie Chart - Donut with Text', - 'chart-pie-label-custom': 'Pie Chart - Custom Label', - 'chart-radar-default': 'Radar Chart', - 'chart-radar-grid-circle-fill': 'Radar Chart - Grid Circle Filled', - 'chart-radar-grid-circle-no-lines': 'Radar Chart - Grid Circle - No lines', - 'chart-radar-grid-fill': 'Radar Chart - Grid Filled', - 'chart-radar-label-custom': 'Radar Chart - Custom Label', - 'chart-radar-radius': 'Radar Chart - Radius Axis', - 'chart-radial-simple': 'Radial Chart', - 'chart-tooltip-indicator-line': 'Tooltip - Line Indicator', - 'chart-tooltip-indicator-none': 'Tooltip - No Indicator', - 'chart-tooltip-label-custom': 'Tooltip - Custom label', - 'chart-tooltip-label-none': 'Tooltip - No Label', -} - -export function getShadcnCatalogSpec(name: string): ShadcnCatalogSpec { - const parts = name.split('-') - const family = parts[1] - if (!isShadcnFamily(family)) { - throw new TypeError(`Unknown shadcn chart family in ${name}`) - } - const variant = parts.slice(2).join('-') - const title = - titleOverrides[name] ?? - `${family === 'tooltip' ? 'Tooltip' : `${titleCase(family)} Chart`} - ${variant.split('-').map(titleCase).join(' ')}` - return { - name, - family, - variant, - title, - description: - (family === 'area' || family === 'bar' || family === 'line') && - variant === 'interactive' - ? 'Showing total visitors for the last 3 months' - : family === 'area' || family === 'radar' - ? 'Showing total visitors for the last 6 months' - : family === 'tooltip' - ? tooltipDescription(variant) - : 'January - June 2024', - footerNote: - family === 'area' || family === 'radar' - ? 'January - June 2024' - : 'Showing total visitors for the last 6 months', - square: family === 'pie' || family === 'radar' || family === 'radial', - legend: - variant.includes('legend') || - variant === 'icons' || - (variant === 'stacked' && family === 'bar') || - (family === 'area' && variant === 'interactive'), - } -} - -function tooltipDescription(variant: string): string { - if (variant === 'advanced') return 'Tooltip with custom formatter and total.' - if (variant === 'default') return 'Default tooltip with ChartTooltipContent.' - if (variant === 'formatter') return 'Tooltip with custom formatter.' - if (variant === 'icons') return 'Tooltip with icons.' - if (variant === 'indicator-line') return 'Tooltip with line indicator.' - if (variant === 'indicator-none') return 'Tooltip with no indicator.' - if (variant === 'label-custom') - return 'Tooltip with custom label from chartConfig.' - if (variant === 'label-formatter') return 'Tooltip with label formatter.' - if (variant === 'label-none') return 'Tooltip with no label.' - return 'A chart tooltip.' -} - -function titleCase(value: string): string { - return value.charAt(0).toUpperCase() + value.slice(1) -} - -function isShadcnFamily(value: string | undefined): value is ShadcnChartFamily { - return ( - value === 'area' || - value === 'bar' || - value === 'line' || - value === 'pie' || - value === 'radar' || - value === 'radial' || - value === 'tooltip' - ) -} +export * from '@charts-poc/demo-data/shadcn' diff --git a/benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx b/benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx index 2c843e2a..b1f162ec 100644 --- a/benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx +++ b/benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx @@ -60,7 +60,7 @@ import { shadcnChartMount, } from './shadcn-chart-card' import { createShadcnSpringRenderer } from './shadcn-motion' -import interactiveAreaData from '../shadcn/area-interactive-data.json' +import interactiveAreaData from '@charts-poc/demo-data/shadcn-area-interactive-data' import { tanstackCase } from './mount' import type { ChartPoint, ChartTooltipOptions } from '@tanstack/charts' import type { ChartDefinition, ChartValue } from '@tanstack/charts' diff --git a/benchmarks/conformance/shared/transforms/learning-poverty.ts b/benchmarks/conformance/shared/transforms/learning-poverty.ts index 2f8d011f..7ea6710d 100644 --- a/benchmarks/conformance/shared/transforms/learning-poverty.ts +++ b/benchmarks/conformance/shared/transforms/learning-poverty.ts @@ -1,96 +1 @@ -import { learningPoverty } from '@charts-poc/demo-data/learning-poverty' -import { geoCentroid } from 'd3-geo' -import { worldCountries } from '../fixtures/country-atlas' -import { simplifyPolygonGeometry } from '../fixtures/simplify-geo' -import type { LearningPovertyRow } from '@charts-poc/demo-data/learning-poverty' -import type { ExtendedFeature, GeoGeometryObjects } from 'd3-geo' -import type { CountryFeature, CountryGeometry } from '../fixtures/country-atlas' - -type PointGeometry = Extract - -export interface LearningPovertyProperties extends LearningPovertyRow { - name: string -} - -export type LearningPovertyCountry = ExtendedFeature< - CountryGeometry, - LearningPovertyProperties -> -export type LearningPovertyPoint = ExtendedFeature< - PointGeometry, - LearningPovertyProperties -> - -// The source uses World Bank names while world-atlas uses Natural Earth names. -// Tiny states absent from the 110m atlas remain unmatched. -const naturalEarthNameBySourceName: Readonly> = { - 'Congo, Dem Rep': 'Dem. Rep. Congo', - 'Congo, Rep': 'Congo', - 'Cote d’Ivoire': "Côte d'Ivoire", - 'Czech Republic': 'Czechia', - 'Dominican Republic': 'Dominican Rep.', - 'Egypt, Arab Rep': 'Egypt', - 'Iran, Islamic Rep': 'Iran', - 'Korea, Rep': 'South Korea', - 'Kyrgyz Republic': 'Kyrgyzstan', - 'Russian Federation': 'Russia', - 'Slovak Republic': 'Slovakia', - 'United States': 'United States of America', - 'Yemen, Rep': 'Yemen', -} - -const countryByName = new Map( - worldCountries.map((country) => [country.properties.name, country]), -) - -export const learningPovertyCountries: readonly LearningPovertyCountry[] = - learningPoverty.flatMap((row) => { - const sourceName = row['Country Name'] - const atlasName = naturalEarthNameBySourceName[sourceName] ?? sourceName - const country = countryByName.get(atlasName) - return country ? [joinCountry(country, row)] : [] - }) - -export const previewLearningPovertyCountries: readonly LearningPovertyCountry[] = - learningPovertyCountries.map((country) => ({ - ...country, - geometry: simplifyPolygonGeometry(country.geometry, 2), - })) - -if (learningPovertyCountries.length !== 95) { - throw new TypeError( - `Expected 95 learning-poverty countries in world-atlas, got ${learningPovertyCountries.length}`, - ) -} - -export const learningPovertyPoints: readonly LearningPovertyPoint[] = - learningPovertyCountries.map((country) => ({ - type: 'Feature', - id: country.id, - geometry: { - type: 'Point', - coordinates: geoCentroid(country), - }, - properties: country.properties, - })) - -// Largest symbols render first so smaller countries remain selectable. -export const learningPovertyPointsByPopulation: readonly LearningPovertyPoint[] = - [...learningPovertyPoints].sort( - (left, right) => right.properties.population - left.properties.population, - ) - -function joinCountry( - country: CountryFeature, - row: LearningPovertyRow, -): LearningPovertyCountry { - return { - type: 'Feature', - id: country.id, - geometry: country.geometry, - properties: { - name: country.properties.name, - ...row, - }, - } -} +export * from '@charts-poc/demo-data/learning-poverty-geography' diff --git a/nx.json b/nx.json index 07c35a6a..0be54a6e 100644 --- a/nx.json +++ b/nx.json @@ -240,6 +240,10 @@ "cache": true, "inputs": ["catalogSource"] }, + "catalog-example-check": { + "cache": true, + "inputs": ["catalogSource"] + }, "catalog-preview-check": { "cache": true, "inputs": ["catalogSource"] diff --git a/package.json b/package.json index 888a1296..cda37b7a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "canvas:check": "node scripts/check-canvas-renderer.mjs", "catalog:index": "node scripts/catalog-index.mjs --write", "catalog:index:check": "nx run charts-workspace:catalog-index-check", + "catalog:examples:clean": "node scripts/clean-catalog-examples.mjs", + "catalog:examples:check": "node scripts/check-catalog-examples.mjs", "catalog:previews": "node scripts/catalog-preview.mjs --write", "catalog:previews:check": "nx run charts-workspace:catalog-preview-check", "shadcn:catalog:check": "node scripts/shadcn-catalog.mjs", diff --git a/packages/charts-demo-data/package.json b/packages/charts-demo-data/package.json index 75657d27..04c805ce 100644 --- a/packages/charts-demo-data/package.json +++ b/packages/charts-demo-data/package.json @@ -61,6 +61,18 @@ "types": "./src/learning-poverty.d.ts", "default": "./src/learning-poverty.js" }, + "./country-atlas": { + "types": "./src/country-atlas.ts", + "default": "./src/country-atlas.ts" + }, + "./learning-poverty-geography": { + "types": "./src/learning-poverty-geography.ts", + "default": "./src/learning-poverty-geography.ts" + }, + "./simplify-geo": { + "types": "./src/simplify-geo.ts", + "default": "./src/simplify-geo.ts" + }, "./metadata": { "types": "./src/metadata.d.ts", "default": "./src/metadata.js" @@ -93,6 +105,13 @@ "types": "./src/simpsons.d.ts", "default": "./src/simpsons.js" }, + "./shadcn": { + "types": "./src/shadcn.ts", + "default": "./src/shadcn.ts" + }, + "./shadcn-area-interactive-data": { + "default": "./src/shadcn-area-interactive-data.json" + }, "./survey": { "types": "./src/survey.d.ts", "default": "./src/survey.js" diff --git a/packages/charts-demo-data/src/country-atlas.ts b/packages/charts-demo-data/src/country-atlas.ts new file mode 100644 index 00000000..e11ea9af --- /dev/null +++ b/packages/charts-demo-data/src/country-atlas.ts @@ -0,0 +1,135 @@ +import countriesAtlasJson from 'world-atlas/countries-110m.json' +import landAtlasJson from 'world-atlas/land-110m.json' +import detailedLandAtlasJson from 'world-atlas/land-50m.json' +import { geoGraticule, geoGraticule10 } from 'd3-geo' +import { feature } from 'topojson-client' +import { simplifyPolygonGeometry } from './simplify-geo' +import type { + ExtendedFeature, + ExtendedFeatureCollection, + GeoGeometryObjects, + GeoSphere, +} from 'd3-geo' + +type AtlasTopology = Parameters[0] + +export type CountryGeometry = Extract< + GeoGeometryObjects, + { type: 'Polygon' | 'MultiPolygon' } +> + +export interface CountryProperties { + name: string +} + +export type CountryFeature = ExtendedFeature +export type LandFeature = ExtendedFeature> + +export const worldSphere: GeoSphere = { type: 'Sphere' } +export const worldGraticule = geoGraticule10() +export const previewWorldGraticule = geoGraticule().step([30, 30])() + +const countriesTopology = atlasTopology( + countriesAtlasJson, + 'world-atlas countries-110m', +) +const countriesObject = countriesTopology.objects.countries +if (!countriesObject) { + throw new TypeError('world-atlas countries-110m is missing countries') +} + +const convertedCountries = feature(countriesTopology, countriesObject) +if (convertedCountries.type !== 'FeatureCollection') { + throw new TypeError('world-atlas countries did not produce a collection') +} + +export const worldCountries: readonly CountryFeature[] = + convertedCountries.features.flatMap((entry) => { + if ( + !isCountryGeometry(entry.geometry) || + !isRecord(entry.properties) || + typeof entry.properties.name !== 'string' + ) { + return [] + } + + return [ + { + type: 'Feature', + id: entry.id === undefined ? entry.properties.name : String(entry.id), + geometry: entry.geometry, + properties: { + name: entry.properties.name, + }, + }, + ] + }) + +if (worldCountries.length !== 177) { + throw new TypeError( + `Expected 177 world-atlas countries, got ${worldCountries.length}`, + ) +} + +export const worldCountryCollection: ExtendedFeatureCollection = + { + type: 'FeatureCollection', + features: [...worldCountries], + } + +export const worldLand = convertLand(landAtlasJson, 'world-atlas land-110m') +export const previewWorldLand: LandFeature = { + ...worldLand, + geometry: simplifyPolygonGeometry(worldLand.geometry, 2), +} +export const detailedWorldLand = convertLand( + detailedLandAtlasJson, + 'world-atlas land-50m', +) + +function atlasTopology(value: unknown, label: string): AtlasTopology { + if (!isAtlasTopology(value)) { + throw new TypeError(`${label} is not valid TopoJSON`) + } + return value +} + +function convertLand(value: unknown, label: string): LandFeature { + const topology = atlasTopology(value, label) + const landObject = topology.objects.land + if (!landObject) { + throw new TypeError(`${label} is missing land`) + } + + const converted = feature(topology, landObject) + const land = + converted.type === 'FeatureCollection' ? converted.features[0] : converted + if (!land || land.type !== 'Feature' || !isCountryGeometry(land.geometry)) { + throw new TypeError(`${label} did not produce polygon geometry`) + } + + return { + type: 'Feature', + geometry: land.geometry, + properties: {}, + } +} + +function isCountryGeometry( + geometry: GeoGeometryObjects, +): geometry is CountryGeometry { + return geometry.type === 'Polygon' || geometry.type === 'MultiPolygon' +} + +function isAtlasTopology(value: unknown): value is AtlasTopology { + return ( + isRecord(value) && + value.type === 'Topology' && + Array.isArray(value.arcs) && + isRecord(value.objects) + ) +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} diff --git a/packages/charts-demo-data/src/learning-poverty-geography.ts b/packages/charts-demo-data/src/learning-poverty-geography.ts new file mode 100644 index 00000000..eb89511d --- /dev/null +++ b/packages/charts-demo-data/src/learning-poverty-geography.ts @@ -0,0 +1,96 @@ +import { learningPoverty } from '@charts-poc/demo-data/learning-poverty' +import { geoCentroid } from 'd3-geo' +import { worldCountries } from './country-atlas' +import { simplifyPolygonGeometry } from './simplify-geo' +import type { LearningPovertyRow } from '@charts-poc/demo-data/learning-poverty' +import type { ExtendedFeature, GeoGeometryObjects } from 'd3-geo' +import type { CountryFeature, CountryGeometry } from './country-atlas' + +type PointGeometry = Extract + +export interface LearningPovertyProperties extends LearningPovertyRow { + name: string +} + +export type LearningPovertyCountry = ExtendedFeature< + CountryGeometry, + LearningPovertyProperties +> +export type LearningPovertyPoint = ExtendedFeature< + PointGeometry, + LearningPovertyProperties +> + +// The source uses World Bank names while world-atlas uses Natural Earth names. +// Tiny states absent from the 110m atlas remain unmatched. +const naturalEarthNameBySourceName: Readonly> = { + 'Congo, Dem Rep': 'Dem. Rep. Congo', + 'Congo, Rep': 'Congo', + 'Cote d’Ivoire': "Côte d'Ivoire", + 'Czech Republic': 'Czechia', + 'Dominican Republic': 'Dominican Rep.', + 'Egypt, Arab Rep': 'Egypt', + 'Iran, Islamic Rep': 'Iran', + 'Korea, Rep': 'South Korea', + 'Kyrgyz Republic': 'Kyrgyzstan', + 'Russian Federation': 'Russia', + 'Slovak Republic': 'Slovakia', + 'United States': 'United States of America', + 'Yemen, Rep': 'Yemen', +} + +const countryByName = new Map( + worldCountries.map((country) => [country.properties.name, country]), +) + +export const learningPovertyCountries: readonly LearningPovertyCountry[] = + learningPoverty.flatMap((row) => { + const sourceName = row['Country Name'] + const atlasName = naturalEarthNameBySourceName[sourceName] ?? sourceName + const country = countryByName.get(atlasName) + return country ? [joinCountry(country, row)] : [] + }) + +export const previewLearningPovertyCountries: readonly LearningPovertyCountry[] = + learningPovertyCountries.map((country) => ({ + ...country, + geometry: simplifyPolygonGeometry(country.geometry, 2), + })) + +if (learningPovertyCountries.length !== 95) { + throw new TypeError( + `Expected 95 learning-poverty countries in world-atlas, got ${learningPovertyCountries.length}`, + ) +} + +export const learningPovertyPoints: readonly LearningPovertyPoint[] = + learningPovertyCountries.map((country) => ({ + type: 'Feature', + id: country.id, + geometry: { + type: 'Point', + coordinates: geoCentroid(country), + }, + properties: country.properties, + })) + +// Largest symbols render first so smaller countries remain selectable. +export const learningPovertyPointsByPopulation: readonly LearningPovertyPoint[] = + [...learningPovertyPoints].sort( + (left, right) => right.properties.population - left.properties.population, + ) + +function joinCountry( + country: CountryFeature, + row: LearningPovertyRow, +): LearningPovertyCountry { + return { + type: 'Feature', + id: country.id, + geometry: country.geometry, + properties: { + name: country.properties.name, + ...row, + }, + } +} diff --git a/benchmarks/conformance/shadcn/area-interactive-data.json b/packages/charts-demo-data/src/shadcn-area-interactive-data.json similarity index 100% rename from benchmarks/conformance/shadcn/area-interactive-data.json rename to packages/charts-demo-data/src/shadcn-area-interactive-data.json diff --git a/packages/charts-demo-data/src/shadcn.ts b/packages/charts-demo-data/src/shadcn.ts new file mode 100644 index 00000000..5030c176 --- /dev/null +++ b/packages/charts-demo-data/src/shadcn.ts @@ -0,0 +1,216 @@ +export type ShadcnChartFamily = + 'area' | 'bar' | 'line' | 'pie' | 'radar' | 'radial' | 'tooltip' + +export interface ShadcnCatalogSpec { + name: string + family: ShadcnChartFamily + variant: string + title: string + description: string + footerNote: string + square: boolean + legend: boolean +} + +export interface ShadcnMonthDatum { + month: string + desktop: number + mobile: number + tablet: number +} + +export interface ShadcnSeriesDatum { + month: string + series: 'desktop' | 'mobile' | 'tablet' | 'other' + value: number +} + +export interface ShadcnBrowserDatum { + browser: string + visitors: number +} + +export interface ShadcnRadarDatum { + month: string + desktop: number + mobile?: number +} + +export interface ShadcnActivityDatum { + date: string + activity: 'running' | 'swimming' + value: number +} + +export const shadcnMonths: readonly ShadcnMonthDatum[] = [ + { month: 'January', desktop: 186, mobile: 80, tablet: 44 }, + { month: 'February', desktop: 305, mobile: 200, tablet: 72 }, + { month: 'March', desktop: 237, mobile: 120, tablet: 58 }, + { month: 'April', desktop: 73, mobile: 190, tablet: 91 }, + { month: 'May', desktop: 209, mobile: 130, tablet: 67 }, + { month: 'June', desktop: 214, mobile: 140, tablet: 82 }, +] + +export const shadcnSeriesRows: readonly ShadcnSeriesDatum[] = + shadcnMonths.flatMap((row) => [ + { month: row.month, series: 'desktop', value: row.desktop }, + { month: row.month, series: 'mobile', value: row.mobile }, + { month: row.month, series: 'tablet', value: row.tablet }, + ]) + +export const shadcnBrowsers: readonly ShadcnBrowserDatum[] = [ + { browser: 'chrome', visitors: 275 }, + { browser: 'safari', visitors: 200 }, + { browser: 'firefox', visitors: 187 }, + { browser: 'edge', visitors: 173 }, + { browser: 'other', visitors: 90 }, +] + +export const shadcnRadarDefault: readonly ShadcnRadarDatum[] = [ + { month: 'January', desktop: 186 }, + { month: 'February', desktop: 305 }, + { month: 'March', desktop: 237 }, + { month: 'April', desktop: 273 }, + { month: 'May', desktop: 209 }, + { month: 'June', desktop: 214 }, +] + +export const shadcnRadarFilled: readonly ShadcnRadarDatum[] = [ + { month: 'January', desktop: 186 }, + { month: 'February', desktop: 285 }, + { month: 'March', desktop: 237 }, + { month: 'April', desktop: 203 }, + { month: 'May', desktop: 209 }, + { month: 'June', desktop: 264 }, +] + +export const shadcnRadarMultiple: readonly ShadcnRadarDatum[] = + shadcnMonths.map(({ month, desktop, mobile }) => ({ + month, + desktop, + mobile, + })) + +export const shadcnRadarLines: readonly ShadcnRadarDatum[] = [ + { month: 'January', desktop: 186, mobile: 160 }, + { month: 'February', desktop: 185, mobile: 170 }, + { month: 'March', desktop: 207, mobile: 180 }, + { month: 'April', desktop: 173, mobile: 160 }, + { month: 'May', desktop: 160, mobile: 190 }, + { month: 'June', desktop: 174, mobile: 204 }, +] + +export const shadcnActivities: readonly ShadcnActivityDatum[] = [ + { date: '2024-07-15', activity: 'running', value: 450 }, + { date: '2024-07-15', activity: 'swimming', value: 300 }, + { date: '2024-07-16', activity: 'running', value: 380 }, + { date: '2024-07-16', activity: 'swimming', value: 420 }, + { date: '2024-07-17', activity: 'running', value: 520 }, + { date: '2024-07-17', activity: 'swimming', value: 120 }, + { date: '2024-07-18', activity: 'running', value: 140 }, + { date: '2024-07-18', activity: 'swimming', value: 550 }, + { date: '2024-07-19', activity: 'running', value: 600 }, + { date: '2024-07-19', activity: 'swimming', value: 350 }, + { date: '2024-07-20', activity: 'running', value: 480 }, + { date: '2024-07-20', activity: 'swimming', value: 400 }, +] + +export const shadcnColors = [ + 'var(--chart-1, var(--ts-chart-1))', + 'var(--chart-2, var(--ts-chart-2))', + 'var(--chart-3, var(--ts-chart-3))', + 'var(--chart-4, var(--ts-chart-4))', + 'var(--chart-5, var(--ts-chart-5))', +] as const + +const titleOverrides: Record = { + 'chart-area-default': 'Area Chart', + 'chart-area-stacked-expand': 'Area Chart - Stacked Expanded', + 'chart-bar-default': 'Bar Chart', + 'chart-bar-label-custom': 'Bar Chart - Custom Label', + 'chart-bar-stacked': 'Bar Chart - Stacked + Legend', + 'chart-line-default': 'Line Chart', + 'chart-line-dots-custom': 'Line Chart - Custom Dots', + 'chart-line-label-custom': 'Line Chart - Custom Label', + 'chart-pie-simple': 'Pie Chart', + 'chart-pie-donut-text': 'Pie Chart - Donut with Text', + 'chart-pie-label-custom': 'Pie Chart - Custom Label', + 'chart-radar-default': 'Radar Chart', + 'chart-radar-grid-circle-fill': 'Radar Chart - Grid Circle Filled', + 'chart-radar-grid-circle-no-lines': 'Radar Chart - Grid Circle - No lines', + 'chart-radar-grid-fill': 'Radar Chart - Grid Filled', + 'chart-radar-label-custom': 'Radar Chart - Custom Label', + 'chart-radar-radius': 'Radar Chart - Radius Axis', + 'chart-radial-simple': 'Radial Chart', + 'chart-tooltip-indicator-line': 'Tooltip - Line Indicator', + 'chart-tooltip-indicator-none': 'Tooltip - No Indicator', + 'chart-tooltip-label-custom': 'Tooltip - Custom label', + 'chart-tooltip-label-none': 'Tooltip - No Label', +} + +export function getShadcnCatalogSpec(name: string): ShadcnCatalogSpec { + const parts = name.split('-') + const family = parts[1] + if (!isShadcnFamily(family)) { + throw new TypeError(`Unknown shadcn chart family in ${name}`) + } + const variant = parts.slice(2).join('-') + const title = + titleOverrides[name] ?? + `${family === 'tooltip' ? 'Tooltip' : `${titleCase(family)} Chart`} - ${variant.split('-').map(titleCase).join(' ')}` + return { + name, + family, + variant, + title, + description: + (family === 'area' || family === 'bar' || family === 'line') && + variant === 'interactive' + ? 'Showing total visitors for the last 3 months' + : family === 'area' || family === 'radar' + ? 'Showing total visitors for the last 6 months' + : family === 'tooltip' + ? tooltipDescription(variant) + : 'January - June 2024', + footerNote: + family === 'area' || family === 'radar' + ? 'January - June 2024' + : 'Showing total visitors for the last 6 months', + square: family === 'pie' || family === 'radar' || family === 'radial', + legend: + variant.includes('legend') || + variant === 'icons' || + (variant === 'stacked' && family === 'bar') || + (family === 'area' && variant === 'interactive'), + } +} + +function tooltipDescription(variant: string): string { + if (variant === 'advanced') return 'Tooltip with custom formatter and total.' + if (variant === 'default') return 'Default tooltip with ChartTooltipContent.' + if (variant === 'formatter') return 'Tooltip with custom formatter.' + if (variant === 'icons') return 'Tooltip with icons.' + if (variant === 'indicator-line') return 'Tooltip with line indicator.' + if (variant === 'indicator-none') return 'Tooltip with no indicator.' + if (variant === 'label-custom') + return 'Tooltip with custom label from chartConfig.' + if (variant === 'label-formatter') return 'Tooltip with label formatter.' + if (variant === 'label-none') return 'Tooltip with no label.' + return 'A chart tooltip.' +} + +function titleCase(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1) +} + +function isShadcnFamily(value: string | undefined): value is ShadcnChartFamily { + return ( + value === 'area' || + value === 'bar' || + value === 'line' || + value === 'pie' || + value === 'radar' || + value === 'radial' || + value === 'tooltip' + ) +} diff --git a/packages/charts-demo-data/src/simplify-geo.ts b/packages/charts-demo-data/src/simplify-geo.ts new file mode 100644 index 00000000..5486e5be --- /dev/null +++ b/packages/charts-demo-data/src/simplify-geo.ts @@ -0,0 +1,130 @@ +import type { GeoGeometryObjects } from 'd3-geo' + +type PolygonGeometry = Extract< + GeoGeometryObjects, + { type: 'Polygon' | 'MultiPolygon' } +> +type Position = number[] + +export function simplifyPolygonGeometry( + geometry: PolygonGeometry, + tolerance: number, +): PolygonGeometry { + if (geometry.type === 'Polygon') { + return { + type: 'Polygon', + coordinates: geometry.coordinates.map((ring) => + simplifyRing(ring, tolerance), + ), + } + } + + return { + type: 'MultiPolygon', + coordinates: geometry.coordinates.map((polygon) => + polygon.map((ring) => simplifyRing(ring, tolerance)), + ), + } +} + +function simplifyRing( + ring: readonly Position[], + tolerance: number, +): Position[] { + if (ring.length <= 4) return [...ring] + + const openRing = ring.slice(0, -1) + const anchor = openRing[0] + if (!anchor) return [...ring] + + let splitIndex = 1 + let farthestDistance = 0 + for (let index = 1; index < openRing.length; index += 1) { + const point = openRing[index] + if (!point) continue + const distance = squaredDistance(anchor, point) + if (distance > farthestDistance) { + farthestDistance = distance + splitIndex = index + } + } + + const firstHalf = simplifyLine( + openRing.slice(0, splitIndex + 1), + tolerance * tolerance, + ) + const secondHalf = simplifyLine( + [...openRing.slice(splitIndex), anchor], + tolerance * tolerance, + ) + const simplified = [...firstHalf.slice(0, -1), ...secondHalf] + + return simplified.length >= 4 ? simplified : [...ring] +} + +function simplifyLine( + points: readonly Position[], + squaredTolerance: number, +): Position[] { + const first = points[0] + const last = points.at(-1) + if (!first || !last || points.length <= 2) return [...points] + + let farthestIndex = 0 + let farthestDistance = squaredTolerance + for (let index = 1; index < points.length - 1; index += 1) { + const point = points[index] + if (!point) continue + const distance = squaredSegmentDistance(point, first, last) + if (distance > farthestDistance) { + farthestDistance = distance + farthestIndex = index + } + } + + if (farthestIndex === 0) return [first, last] + + const left = simplifyLine( + points.slice(0, farthestIndex + 1), + squaredTolerance, + ) + const right = simplifyLine(points.slice(farthestIndex), squaredTolerance) + return [...left.slice(0, -1), ...right] +} + +function squaredSegmentDistance( + point: Position, + start: Position, + end: Position, +): number { + const [pointX = 0, pointY = 0] = point + let [x = 0, y = 0] = start + const [endX = 0, endY = 0] = end + let dx = endX - x + let dy = endY - y + + if (dx !== 0 || dy !== 0) { + const progress = + ((pointX - x) * dx + (pointY - y) * dy) / (dx * dx + dy * dy) + if (progress > 1) { + x = endX + y = endY + } else if (progress > 0) { + x += dx * progress + y += dy * progress + } + dx = pointX - x + dy = pointY - y + } else { + dx = pointX - x + dy = pointY - y + } + + return dx * dx + dy * dy +} + +function squaredDistance(left: Position, right: Position): number { + const dx = (left[0] ?? 0) - (right[0] ?? 0) + const dy = (left[1] ?? 0) - (right[1] ?? 0) + return dx * dx + dy * dy +} diff --git a/project.json b/project.json index 4ee41c88..ff2f1ca4 100644 --- a/project.json +++ b/project.json @@ -128,6 +128,12 @@ "command": "node scripts/catalog-index.mjs --check" } }, + "catalog-example-check": { + "executor": "nx:run-commands", + "options": { + "command": "node scripts/check-catalog-examples.mjs" + } + }, "catalog-preview-check": { "executor": "nx:run-commands", "options": { @@ -170,6 +176,7 @@ "package-check", "bundle-check", "benchmark-check", + "catalog-example-check", "catalog-index-check", "catalog-preview-check", "conformance-app-build" @@ -195,6 +202,7 @@ "react-native-types", "bundle-check", "benchmark-check", + "catalog-example-check", "catalog-index-check", "catalog-preview-check", "conformance-app-build" diff --git a/scripts/catalog-definition-shapes.test.mjs b/scripts/catalog-definition-shapes.test.mjs index 1b1c2acf..6892e67d 100644 --- a/scripts/catalog-definition-shapes.test.mjs +++ b/scripts/catalog-definition-shapes.test.mjs @@ -9,11 +9,42 @@ const casesDirectory = path.resolve( ) const responsiveDefinitions = [ - '29-waterfall/tanstack.ts', - '85-scrollable-resource-lanes/tanstack.ts', - '92-editable-event-range/tanstack.ts', - 'bar-grouped/tanstack.ts', - 'bar-vertical-sorted/tanstack.ts', + '137-shadcn-area-interactive/example.tsx', + '152-shadcn-line-default/example.tsx', + '153-shadcn-line-dots-colors/example.tsx', + '154-shadcn-line-dots-custom/example.tsx', + '155-shadcn-line-dots/example.tsx', + '156-shadcn-line-interactive/example.tsx', + '157-shadcn-line-label-custom/example.tsx', + '158-shadcn-line-label/example.tsx', + '159-shadcn-line-linear/example.tsx', + '160-shadcn-line-multiple/example.tsx', + '161-shadcn-line-step/example.tsx', + '181-shadcn-radar-label-custom/example.tsx', + '186-shadcn-radial-label/example.tsx', + '29-waterfall/example.tsx', + '85-scrollable-resource-lanes/example.tsx', + '92-editable-event-range/example.tsx', + 'bar-grouped/example.tsx', + 'bar-vertical-sorted/example.tsx', +] + +const indirectDefinitions = [ + '130-shadcn-radar-multiple/example.tsx', + '172-shadcn-radar-default/example.tsx', + '173-shadcn-radar-dots/example.tsx', + '174-shadcn-radar-grid-circle-fill/example.tsx', + '175-shadcn-radar-grid-circle-no-lines/example.tsx', + '176-shadcn-radar-grid-circle/example.tsx', + '177-shadcn-radar-grid-custom/example.tsx', + '178-shadcn-radar-grid-fill/example.tsx', + '179-shadcn-radar-grid-none/example.tsx', + '180-shadcn-radar-icons/example.tsx', + '182-shadcn-radar-legend/example.tsx', + '183-shadcn-radar-lines-only/example.tsx', + '184-shadcn-radar-radius/example.tsx', + '185-shadcn-radial-grid/example.tsx', + '188-shadcn-radial-simple/example.tsx', ] describe('catalog definition shapes', () => { @@ -27,23 +58,18 @@ describe('catalog definition shapes', () => { caseDirectories.map(async (directory) => { const caseEntries = await readdir(directory, { withFileTypes: true }) return caseEntries - .filter( - (entry) => - entry.isFile() && - (entry.name === 'tanstack.ts' || - entry.name === 'tanstack-view.tsx' || - entry.name === 'view.tsx' || - entry.name === 'chart.ts'), - ) + .filter((entry) => entry.isFile() && entry.name === 'example.tsx') .map((entry) => path.join(directory, entry.name)) }), ) ).flat() const classification = { + indirect: [], parameterless: [], responsive: [], static: 0, + staticFiles: [], } await Promise.all( @@ -58,16 +84,26 @@ describe('catalog definition shapes', () => { ) expect(classification.parameterless).toEqual([]) - expect(classification.static).toBe(123) + expect(classification.static).toBe(160) expect(classification.responsive.sort()).toEqual(responsiveDefinitions) - expect(classification.static + classification.responsive.length).toBe(128) + expect(classification.indirect.sort()).toEqual(indirectDefinitions) + expect(classification.staticFiles).toHaveLength(155) + expect( + new Set([ + ...classification.staticFiles, + ...classification.responsive, + ...classification.indirect, + ]).size, + ).toBe(files.length) }) it('classifies the base definition once when options are added', () => { const classification = { + indirect: [], parameterless: [], responsive: [], static: 0, + staticFiles: [], } classifyDefinitions( @@ -84,9 +120,11 @@ describe('catalog definition shapes', () => { ) expect(classification).toEqual({ + indirect: [], parameterless: [], responsive: ['two-argument.ts'], static: 2, + staticFiles: ['two-argument.ts'], }) }) }) @@ -98,6 +136,9 @@ function classifyDefinitions(relativePath, source, classification) { ts.ScriptTarget.Latest, true, ) + let hasIndirectDefinition = false + let hasResponsiveDefinition = false + let hasStaticDefinition = false function visit(node) { if ( @@ -106,6 +147,7 @@ function classifyDefinitions(relativePath, source, classification) { node.expression.text === 'facetChart' ) { classification.static += 1 + hasStaticDefinition = true } if ( @@ -118,6 +160,7 @@ function classifyDefinitions(relativePath, source, classification) { if (ts.isObjectLiteralExpression(definition)) { classification.static += 1 + hasStaticDefinition = true } else if (ts.isArrowFunction(definition)) { if (definition.parameters.length === 0) { const { line } = sourceFile.getLineAndCharacterOfPosition( @@ -126,7 +169,14 @@ function classifyDefinitions(relativePath, source, classification) { classification.parameterless.push(`${relativePath}:${line + 1}`) } else { classification.responsive.push(relativePath) + hasResponsiveDefinition = true } + } else if (!( + ts.isCallExpression(definition) && + ts.isIdentifier(definition.expression) && + definition.expression.text === 'defineChart' + )) { + hasIndirectDefinition = true } } @@ -134,6 +184,14 @@ function classifyDefinitions(relativePath, source, classification) { } visit(sourceFile) + if (hasStaticDefinition) classification.staticFiles.push(relativePath) + if ( + hasIndirectDefinition && + !hasStaticDefinition && + !hasResponsiveDefinition + ) { + classification.indirect.push(relativePath) + } } function unwrapParentheses(node) { diff --git a/scripts/catalog-index.mjs b/scripts/catalog-index.mjs index a4a5be14..a2718507 100644 --- a/scripts/catalog-index.mjs +++ b/scripts/catalog-index.mjs @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url' import { format as formatWithPrettier } from 'prettier' import { parseConformanceCaseMeta } from '../benchmarks/conformance/metadata.ts' -export const catalogIndexSchemaVersion = 1 +export const catalogIndexSchemaVersion = 2 export const catalogIndexSourceRepository = 'tanstack/charts' export const catalogIndexSourcePathRoot = 'benchmarks/conformance/' @@ -82,22 +82,13 @@ export function createCatalogIndex(cases, collectionsByCaseId = new Map()) { cases: [...cases] .sort((left, right) => left.metadata.order - right.metadata.order) .map(({ metadata }) => { - const referenceRenderer = - metadata.referenceRenderer ?? 'observable-plot' const collection = collectionsByCaseId.get(metadata.id) return { ...metadata, ...(collection ? { collection } : {}), entries: { - tanstack: caseSourcePath(metadata.id, 'tanstack'), - reference: { - renderer: referenceRenderer, - path: caseSourcePath( - metadata.id, - rendererFileName(referenceRenderer), - ), - }, + example: caseSourcePath(metadata.id, 'example'), }, } }), @@ -170,14 +161,10 @@ export function validateCatalogIndex(index) { orders.add(metadata.order) previousOrder = metadata.order - const referenceRenderer = metadata.referenceRenderer ?? 'observable-plot' assert( isRecord(entry.entries) && - entry.entries.tanstack === caseSourcePath(metadata.id, 'tanstack') && - isRecord(entry.entries.reference) && - entry.entries.reference.renderer === referenceRenderer && - entry.entries.reference.path === - caseSourcePath(metadata.id, rendererFileName(referenceRenderer)), + Object.keys(entry.entries).length === 1 && + entry.entries.example === caseSourcePath(metadata.id, 'example'), `catalog index case ${metadata.id} has invalid source entries`, ) assert( @@ -262,10 +249,7 @@ export async function checkCatalogIndex() { async function validateCatalogIndexSourceEntries(index) { for (const entry of index.cases) { - for (const sourcePath of [ - entry.entries.tanstack, - entry.entries.reference.path, - ]) { + for (const sourcePath of [entry.entries.example]) { const sourceFile = path.join(rootDirectory, ...sourcePath.split('/')) let stats try { @@ -310,12 +294,9 @@ function validateAuthoredCases(cases) { } } -function rendererFileName(renderer) { - return renderer === 'observable-plot' ? 'plot' : renderer -} - function caseSourcePath(id, rendererFile) { - return `benchmarks/conformance/cases/${id}/${rendererFile}.ts` + const extension = rendererFile === 'example' ? 'tsx' : 'ts' + return `benchmarks/conformance/cases/${id}/${rendererFile}.${extension}` } function isRecord(value) { diff --git a/scripts/catalog-index.test.mjs b/scripts/catalog-index.test.mjs index a688a814..c1ef37da 100644 --- a/scripts/catalog-index.test.mjs +++ b/scripts/catalog-index.test.mjs @@ -22,7 +22,7 @@ describe('catalog index', () => { expect(validateCatalogIndex(index)).toEqual({ caseCount: 1 }) expect(index).toEqual({ - schemaVersion: 1, + schemaVersion: 2, source: { repo: 'tanstack/charts', pathRoot: 'benchmarks/conformance/', @@ -31,11 +31,7 @@ describe('catalog index', () => { { ...metadata, entries: { - tanstack: 'benchmarks/conformance/cases/01-line/tanstack.ts', - reference: { - renderer: 'observable-plot', - path: 'benchmarks/conformance/cases/01-line/plot.ts', - }, + example: 'benchmarks/conformance/cases/01-line/example.tsx', }, }, ], @@ -69,8 +65,8 @@ describe('catalog index', () => { it('rejects source entries that drift from the case ID', () => { const index = createCatalogIndex(cases) - index.cases[0].entries.tanstack = - 'benchmarks/conformance/cases/other/tanstack.ts' + index.cases[0].entries.example = + 'benchmarks/conformance/cases/other/example.tsx' expect(() => validateCatalogIndex(index)).toThrow( 'catalog index case 01-line has invalid source entries', diff --git a/scripts/check-catalog-examples.mjs b/scripts/check-catalog-examples.mjs new file mode 100644 index 00000000..372d83f0 --- /dev/null +++ b/scripts/check-catalog-examples.mjs @@ -0,0 +1,215 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import ts from 'typescript' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const casesRoot = path.join(root, 'benchmarks', 'conformance', 'cases') +const sourceExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.css'] +const forbiddenPublicNames = + /\b(?:Conformance|tanstackCase|tanstackMount|reactMount|catalogPreviewDefinition)\b/ + +const directories = (await fs.readdir(casesRoot, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort() + +const failures = [] +const closureSizes = [] +const publicTypeScriptPaths = new Set() + +for (const directory of directories) { + const caseRoot = path.join(casesRoot, directory) + const examplePath = path.join(caseRoot, 'example.tsx') + const adapterPath = path.join(caseRoot, 'tanstack.ts') + + if (!(await isFile(examplePath))) { + failures.push(`${directory}: missing example.tsx`) + continue + } + if (!(await isFile(adapterPath))) { + failures.push(`${directory}: missing tanstack.ts adapter`) + continue + } + + const adapterSource = await fs.readFile(adapterPath, 'utf8') + if (!fromExample(adapterSource)) { + failures.push(`${directory}: tanstack.ts does not import ./example`) + } + + const visited = new Set() + const pending = [examplePath] + while (pending.length > 0) { + const sourcePath = pending.pop() + if (!sourcePath || visited.has(sourcePath)) continue + visited.add(sourcePath) + + const source = await fs.readFile(sourcePath, 'utf8') + const relativePath = path.relative(caseRoot, sourcePath) + if (/\.(?:ts|tsx)$/u.test(sourcePath)) { + publicTypeScriptPaths.add(sourcePath) + } + if (forbiddenPublicNames.test(source)) { + failures.push( + `${directory}/${relativePath}: contains conformance-only code`, + ) + } + if ( + sourcePath === examplePath && + !hasDefaultComponentExport(source, sourcePath) + ) { + failures.push( + `${directory}: example.tsx must default-export a React component function`, + ) + } + if (sourcePath === examplePath && definitionFollowsComponent(source)) { + failures.push( + `${directory}: put the chart definition before the component shell`, + ) + } + + for (const specifier of importSpecifiers(source, sourcePath)) { + if (!specifier.startsWith('.')) continue + const resolved = await resolveImport(path.dirname(sourcePath), specifier) + if (!resolved) { + failures.push( + `${directory}/${relativePath}: cannot resolve ${JSON.stringify(specifier)}`, + ) + continue + } + const relativeTarget = path.relative(caseRoot, resolved) + if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) { + failures.push( + `${directory}/${relativePath}: public import leaves the case directory (${specifier})`, + ) + continue + } + if (/\.(?:ts|tsx|js|jsx)$/.test(resolved)) pending.push(resolved) + } + } + closureSizes.push(visited.size) +} + +checkUnusedPublicSource(publicTypeScriptPaths, failures) + +if (failures.length > 0) { + throw new Error(`Catalog example contract failed:\n${failures.join('\n')}`) +} + +console.log( + `Validated ${directories.length} self-contained catalog example entries (largest TypeScript closure: ${Math.max(...closureSizes)} files).`, +) + +function fromExample(source) { + return /(?:from\s*|import\s*)['"]\.\/example['"]/.test(source) +} + +function hasDefaultComponentExport(source, sourcePath) { + const file = ts.createSourceFile( + sourcePath, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ) + return file.statements.some( + (statement) => + ts.isFunctionDeclaration(statement) && + statement.modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword, + ), + ) +} + +function definitionFollowsComponent(source) { + const definitionIndex = source.indexOf('defineChart(') + const componentIndex = source.indexOf('export default function') + return ( + definitionIndex !== -1 && + componentIndex !== -1 && + definitionIndex > componentIndex + ) +} + +function checkUnusedPublicSource(sourcePaths, output) { + const config = ts.readConfigFile( + path.join(root, 'tsconfig.json'), + ts.sys.readFile, + ) + if (config.error) { + output.push(ts.flattenDiagnosticMessageText(config.error.messageText, '\n')) + return + } + const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root) + const program = ts.createProgram({ + rootNames: [...sourcePaths], + options: { + ...parsed.options, + noEmit: true, + noUnusedLocals: true, + noUnusedParameters: false, + }, + }) + const unusedCodes = new Set([6133, 6192, 6196, 6198]) + for (const diagnostic of program.getSemanticDiagnostics()) { + if ( + !diagnostic.file || + !sourcePaths.has(diagnostic.file.fileName) || + !unusedCodes.has(diagnostic.code) + ) { + continue + } + const position = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start ?? 0, + ) + output.push( + `${path.relative(casesRoot, diagnostic.file.fileName)}:${position.line + 1}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`, + ) + } +} + +function importSpecifiers(source, sourcePath) { + const file = ts.createSourceFile( + sourcePath, + source, + ts.ScriptTarget.Latest, + true, + sourcePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ) + const specifiers = [] + for (const statement of file.statements) { + if ( + (ts.isImportDeclaration(statement) || + ts.isExportDeclaration(statement)) && + statement.moduleSpecifier && + ts.isStringLiteral(statement.moduleSpecifier) + ) { + specifiers.push(statement.moduleSpecifier.text) + } + } + return specifiers +} + +async function resolveImport(parent, specifier) { + const target = path.resolve(parent, specifier) + const candidates = path.extname(target) + ? [target] + : [ + ...sourceExtensions.map((extension) => `${target}${extension}`), + ...sourceExtensions.map((extension) => + path.join(target, `index${extension}`), + ), + ] + for (const candidate of candidates) { + if (await isFile(candidate)) return candidate + } + return null +} + +async function isFile(filePath) { + try { + return (await fs.stat(filePath)).isFile() + } catch { + return false + } +} diff --git a/scripts/clean-catalog-examples.mjs b/scripts/clean-catalog-examples.mjs new file mode 100644 index 00000000..0f2cb464 --- /dev/null +++ b/scripts/clean-catalog-examples.mjs @@ -0,0 +1,28 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { format } from 'prettier' +import { removeUnusedTypeScript } from './typescript-source.mjs' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const casesRoot = path.join(root, 'benchmarks', 'conformance', 'cases') +const directories = (await fs.readdir(casesRoot, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort() + +let changed = 0 +for (const directory of directories) { + const examplePath = path.join(casesRoot, directory, 'example.tsx') + const source = await fs.readFile(examplePath, 'utf8') + const cleaned = await format(removeUnusedTypeScript(source, examplePath), { + parser: 'typescript', + semi: false, + singleQuote: true, + }) + if (cleaned === source) continue + await fs.writeFile(examplePath, cleaned, 'utf8') + changed += 1 +} + +console.log(`Cleaned ${changed} of ${directories.length} catalog examples.`) diff --git a/scripts/demo-data.test.mjs b/scripts/demo-data.test.mjs index 40f1fdb4..b68e1ef2 100644 --- a/scripts/demo-data.test.mjs +++ b/scripts/demo-data.test.mjs @@ -11,6 +11,13 @@ import { } from '../packages/charts-demo-data/src/parse-csv.js' const workspace = resolve(import.meta.dirname, '..') +const supportExports = [ + './country-atlas', + './learning-poverty-geography', + './shadcn', + './shadcn-area-interactive-data', + './simplify-geo', +] describe('demo data', () => { it('parses quoted CSV rows without code generation', () => { @@ -33,11 +40,20 @@ describe('demo data', () => { 'utf8', ), ) - const datasetExports = Object.keys(packageJson.exports) - .filter((specifier) => specifier !== './metadata') + const exactExports = Object.keys(packageJson.exports) + const datasetExports = exactExports + .filter( + (specifier) => + specifier !== './metadata' && !supportExports.includes(specifier), + ) .map((specifier) => specifier.slice(2)) .sort() + expect( + exactExports + .filter((specifier) => supportExports.includes(specifier)) + .sort(), + ).toEqual([...supportExports].sort()) expect(demoDatasetMetadata.map(({ id }) => id).sort()).toEqual( datasetExports, ) diff --git a/scripts/generate-shadcn-cases.mjs b/scripts/generate-shadcn-cases.mjs index cf9dbb4d..8cbeffdd 100644 --- a/scripts/generate-shadcn-cases.mjs +++ b/scripts/generate-shadcn-cases.mjs @@ -1,6 +1,11 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { format } from 'prettier' +import { getShadcnCatalogSpec } from '@charts-poc/demo-data/shadcn' +import { + shadcnExampleSource, + shadcnExampleStyles, +} from './shadcn-example-source.mjs' const root = path.resolve(import.meta.dirname, '..') const catalogPath = path.join( @@ -10,13 +15,6 @@ const catalogPath = path.join( const casesRoot = path.join(root, 'benchmarks/conformance/cases') const coverageRoot = path.join(root, 'benchmarks/conformance') const catalog = JSON.parse(await readFile(catalogPath, 'utf8')) -const handAuthoredCases = new Set([ - 'chart-bar-multiple', - 'chart-pie-donut-text', - 'chart-radar-multiple', - 'chart-radial-text', - 'chart-tooltip-advanced', -]) let nextCaseNumber = Math.max( 126, @@ -32,18 +30,51 @@ for (const entry of catalog.cases) { nextCaseNumber += 1 } const caseNumber = Number(entry.localCaseId.match(/^(\d+)-/u)?.[1] ?? 0) - if (handAuthoredCases.has(entry.name)) continue const id = entry.localCaseId const directory = path.join(casesRoot, id) generatedCount += 1 await mkdir(directory, { recursive: true }) await writeFile( path.join(directory, 'case.json'), - `${JSON.stringify(metadata(entry, id, caseNumber), null, 2)}\n`, + await format( + `${JSON.stringify(metadata(entry, id, caseNumber), null, 2)}\n`, + { + parser: 'json', + }, + ), ) await writeFile( path.join(directory, 'tanstack.ts'), - `import { createShadcnTanStackExample } from '../../shared/shadcn-catalog-tanstack'\n\nconst example = createShadcnTanStackExample('${entry.name}')\n\nexport const shadcnDefinition = example.definition\nexport const catalogCase = example.catalogCase\nexport const mount = example.mount\n`, + await format( + `import { createElement } from 'react' +import Example, { definition } from './example' +import { shadcnChartMount } from '../../shared/shadcn-chart-card' +import { tanstackExampleMount } from '../../shared/mount' +import type { ConformanceInput } from '../../types' + +const ConformanceExample = ({ input }: { input: ConformanceInput }) => + createElement(Example, { width: input.width, height: input.height }) + +export * from './example' +export const shadcnDefinition = definition +${legacyDefinitionAlias(entry.name)} +export const mount = shadcnChartMount(ConformanceExample) +export const catalogCase = tanstackExampleMount( + () => definition, + ${JSON.stringify(`${officialTitle(entry.name)} implemented with TanStack Charts`)}, + { guides: ${getShadcnCatalogSpec(entry.name).family !== 'pie'}, margin: true }, +) +`, + { parser: 'typescript', semi: false, singleQuote: true }, + ), + ) + await writeFile( + path.join(directory, 'example.tsx'), + await shadcnExampleSource(getShadcnCatalogSpec(entry.name)), + ) + await writeFile( + path.join(directory, 'styles.css'), + await shadcnExampleStyles(), ) await writeFile( path.join(directory, 'recharts.ts'), @@ -51,6 +82,18 @@ for (const entry of catalog.cases) { ) } +function legacyDefinitionAlias(name) { + const aliases = { + 'chart-bar-multiple': 'barMultipleDefinition', + 'chart-pie-donut-text': 'pieDonutTextDefinition', + 'chart-radar-multiple': 'radarMultipleDefinition', + 'chart-radial-text': 'radialTextDefinition', + 'chart-tooltip-advanced': 'advancedTooltipDefinition', + } + const alias = aliases[name] + return alias ? `export const ${alias} = definition` : '' +} + await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`) await syncDefinitionCoverage() console.log(`Synchronized ${generatedCount} generated shadcn chart cases.`) @@ -81,7 +124,10 @@ async function syncDefinitionCoverage() { } retainedCases.splice(insertionIndex, 0, ...generatedEntries) roadmap.cases = retainedCases - await writeFile(roadmapPath, `${JSON.stringify(roadmap, null, 2)}\n`) + await writeFile( + roadmapPath, + await format(`${JSON.stringify(roadmap, null, 2)}\n`, { parser: 'json' }), + ) const counts = countCoverage(roadmap.cases) const auditPath = path.join(coverageRoot, roadmap.audit) @@ -157,6 +203,16 @@ async function syncDefinitionCoverage() { function coverageEntry(entry) { const id = entry.localCaseId + const applicationSources = + entry.name === 'dashboard' + ? [ + `benchmarks/conformance/cases/${id}/example.tsx`, + `benchmarks/conformance/cases/${id}/dashboard.tsx`, + ] + : [ + `benchmarks/conformance/cases/${id}/example.tsx`, + `benchmarks/conformance/cases/${id}/styles.css`, + ] return { id, coverage: 'app-composed', @@ -164,24 +220,24 @@ function coverageEntry(entry) { phase: 'phase-0', status: 'verified', capabilities: ['current-definition-api'], - evidence: [`cases/${id}/tanstack.ts`, `cases/${id}/case.json`], + evidence: [`cases/${id}/example.tsx`, `cases/${id}/case.json`], work: [ { kind: 'definition-composition', stage: 'definition-builder', owner: 'charts', coordinateSpace: 'none', - sources: ['benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx'], - summary: `Compose the pinned shadcn ${entry.name} variant through the shared native ${entry.family} chart definition.`, + sources: [`benchmarks/conformance/cases/${id}/example.tsx`], + summary: `Define the pinned shadcn ${entry.name} variant as a self-contained native ${entry.family} chart example.`, }, { kind: 'application-shell', stage: 'post-render', owner: 'application', coordinateSpace: 'dom', - sources: ['benchmarks/conformance/shared/shadcn-chart-card.tsx'], + sources: applicationSources, summary: - 'Share the canonical shadcn card dimensions, typography, footer, and theme tokens across renderer implementations.', + 'Keep the canonical shadcn card dimensions, typography, footer, and theme tokens with the public example.', }, ], } @@ -189,7 +245,7 @@ function coverageEntry(entry) { function auditRow(entry) { const label = entry.id.replace('-shadcn-', ' — shadcn ') - return `| [${label}](./cases/${entry.id}/tanstack.ts) | Definition now | Shared native TanStack Charts ${entry.id.split('-')[2]} definitions reproduce the pinned shadcn variant; the card remains application presentation. |` + return `| [${label}](./cases/${entry.id}/example.tsx) | Definition now | The self-contained native TanStack Charts ${entry.id.split('-')[2]} example reproduces the pinned shadcn variant and card presentation. |` } function overviewRow(entry) { @@ -197,7 +253,7 @@ function overviewRow(entry) { (candidate) => candidate.localCaseId === entry.id, ) const title = metadata ? officialTitle(metadata.name) : 'Dashboard' - return `| [${entry.id} — ${title}](./cases/${entry.id}/tanstack.ts) | Definition now | \`current-definition-api\` | \`tanstack.ts\`, \`case.json\` |` + return `| [${entry.id} — ${title}](./cases/${entry.id}/example.tsx) | Definition now | \`current-definition-api\` | \`example.tsx\`, \`case.json\` |` } function replaceGeneratedRows(source, generatedIds, rows, afterId) { diff --git a/scripts/shadcn-example-source.mjs b/scripts/shadcn-example-source.mjs new file mode 100644 index 00000000..559d8de8 --- /dev/null +++ b/scripts/shadcn-example-source.mjs @@ -0,0 +1,858 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import ts from 'typescript' +import { format } from 'prettier' +import { removeUnusedTypeScript } from './typescript-source.mjs' + +const root = path.resolve(import.meta.dirname, '..') +const implementationPath = path.join( + root, + 'benchmarks/conformance/shared/shadcn-catalog-tanstack.tsx', +) +const cardPath = path.join( + root, + 'benchmarks/conformance/shared/shadcn-chart-card.tsx', +) +const implementationSource = await readFile(implementationPath, 'utf8') +const implementationFile = ts.createSourceFile( + implementationPath, + implementationSource, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, +) +const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) + +const functions = new Map() +const variables = new Map() +for (const statement of implementationFile.statements) { + if (ts.isFunctionDeclaration(statement) && statement.name) { + functions.set(statement.name.text, statement) + } + if ( + ts.isVariableStatement(statement) && + statement.declarationList.declarations.length === 1 + ) { + const declaration = statement.declarationList.declarations[0] + if (ts.isIdentifier(declaration.name)) { + variables.set(declaration.name.text, statement) + } + } +} + +const horizontalVariants = new Set(['horizontal', 'label-custom', 'mixed']) + +export async function shadcnExampleSource(spec) { + const definitionSource = definitionFor(spec) + const componentSource = componentFor(spec) + const body = `${definitionSource}\n\n${componentSource}` + .replaceAll(/\.\.\.\[\],?/gu, '') + .replaceAll(/\.\.\.\{\},?/gu, '') + const imports = importsFor(body) + const formatted = await format( + `${imports}\nimport './styles.css'\n\n${body}`, + { parser: 'typescript', semi: false, singleQuote: true }, + ) + const cleaned = await cleanupGeneratedSource(formatted) + return format( + removeUnusedTypeScript( + cleaned, + path.join( + root, + 'benchmarks/conformance/cases', + `generated-${spec.name}`, + 'example.tsx', + ), + ), + { parser: 'typescript', semi: false, singleQuote: true }, + ) +} + +async function cleanupGeneratedSource(source) { + let file = ts.createSourceFile( + 'example.tsx', + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ) + for (let pass = 0; pass < 3; pass += 1) { + const result = ts.transform(file, [ + (context) => { + const visit = (node) => { + const visited = ts.visitEachChild(node, visit, context) + if (ts.isArrayLiteralExpression(visited)) { + return ts.factory.updateArrayLiteralExpression( + visited, + visited.elements.flatMap((element) => + ts.isSpreadElement(element) && + ts.isArrayLiteralExpression(element.expression) + ? [...element.expression.elements] + : [element], + ), + ) + } + return visited + } + return (node) => ts.visitNode(node, visit) + }, + ]) + file = result.transformed[0] + result.dispose() + } + return format(printer.printFile(file), { + parser: 'typescript', + semi: false, + singleQuote: true, + }) +} + +export async function shadcnExampleStyles() { + const cardSource = await readFile(cardPath, 'utf8') + const match = cardSource.match( + /export const shadcnChartCardStyles = `([\s\S]*?)`\s*$/u, + ) + if (!match) throw new Error('Could not read the shadcn chart-card styles.') + return format(`${match[1].trim()}\n`, { parser: 'css', singleQuote: true }) +} + +function definitionFor(spec) { + const interactive = spec.variant === 'interactive' + const rootName = interactive + ? { + area: 'buildInteractiveAreaDefinition', + bar: 'buildInteractiveBarDefinition', + line: 'buildInteractiveLineDefinition', + pie: 'buildInteractivePieDefinition', + }[spec.family] + : { + area: 'buildAreaDefinition', + bar: 'buildBarDefinition', + line: 'buildLineDefinition', + pie: 'buildPieDefinition', + radar: 'buildRadarDefinition', + radial: 'buildRadialDefinition', + tooltip: 'buildTooltipDefinition', + }[spec.family] + if (!rootName) throw new Error(`Unsupported shadcn example ${spec.name}`) + + const functionNames = new Set([rootName]) + if (!interactive && spec.family === 'area') { + functionNames.add('shadcnPointXAxis').add('shadcnTheme') + } + if (!interactive && spec.family === 'bar') { + if (spec.variant === 'multiple') functionNames.add('groupedBarDefinition') + functionNames + .add('shadcnXAxis') + .add('shadcnBrowserXAxis') + .add('shadcnBrowserYAxis') + .add('shadcnTheme') + .add('titleCase') + } + if (!interactive && spec.family === 'line') { + functionNames.add('shadcnPointXAxis').add('shadcnTheme').add('titleCase') + } + if (!interactive && spec.family === 'pie') { + if (spec.variant === 'stacked') + functionNames.add('buildStackedPieDefinition') + functionNames.add('titleCase') + } + if (spec.family === 'radial') { + functionNames + .add('buildSingleRadialDefinition') + .add('buildStackedRadialDefinition') + .add('radialCenterLabels') + .add('rechartsPolarAngle') + .add('titleCase') + } + if (interactive && spec.family === 'area') { + functionNames + .add('filterInteractiveAreaRows') + .add('interactiveDateTicks') + .add('formatMonthDay') + .add('shadcnTheme') + } + if (interactive && (spec.family === 'bar' || spec.family === 'line')) { + functionNames.add('formatMonthDay').add('shadcnTheme') + } + if (spec.family === 'tooltip') { + functionNames.add('formatWeekday').add('shadcnTheme').add('titleCase') + } else { + functionNames + .add('shadcnTooltipContent') + .add('browserMetric') + .add('titleCase') + } + if (spec.family === 'tooltip') { + functionNames.add('ShadcnTooltipBody').add('ShadcnActivityIcon') + } + + const variableNames = new Set([ + 'monthSeries', + 'twoSeries', + 'browserNames', + 'activityNames', + 'horizontalVariants', + 'interactiveAreaRows', + 'interactiveBarRows', + 'interactivePieRows', + ]) + const printedFunctions = [...functionNames] + .map((name) => { + const declaration = functions.get(name) + if (!declaration) throw new Error(`Missing shadcn helper ${name}`) + return printSpecializedFunction(declaration, spec, name === rootName) + }) + .join('\n\n') + const selectedVariables = new Set() + let variableSearchSource = printedFunctions + for (let pass = 0; pass < variableNames.size; pass += 1) { + for (const name of variableNames) { + if ( + selectedVariables.has(name) || + !new RegExp(`\\b${name}\\b`, 'u').test(variableSearchSource) + ) { + continue + } + selectedVariables.add(name) + variableSearchSource += `\n${printer.printNode( + ts.EmitHint.Unspecified, + variables.get(name), + implementationFile, + )}` + } + } + const usedVariables = [...selectedVariables] + .sort( + (left, right) => + variables.get(left).getStart(implementationFile) - + variables.get(right).getStart(implementationFile), + ) + .map((name) => + printer.printNode( + ts.EmitHint.Unspecified, + variables.get(name), + implementationFile, + ), + ) + .join('\n') + + const state = interactiveState(spec) + const focus = + spec.family === 'pie' || spec.family === 'radar' || spec.family === 'radial' + ? 'focusGroupAngle' + : "'group-x'" + const tooltipContent = + spec.family === 'tooltip' + ? '() => ({ rows: [] })' + : '(points) => shadcnTooltipContent(points)' + const createCall = state + ? `createDefinition(${state.parameter})` + : 'createDefinition()' + const createSignature = state + ? `(${state.parameter}: ${state.type} = ${state.defaultValue})` + : '()' + + return `${typeAliasesFor(spec)}${usedVariables}\n\n${printedFunctions}\n\nexport function createExampleChart${createSignature} { + return defineChart(${createCall}, { + svgAnimation: false, + focus: ${focus}, + keyboard: ${spec.family !== 'radial'}, + tooltip: { + use: tooltip, + className: 'sc-chart-tooltip', + anchor: ${spec.family === 'tooltip' ? '(_points, context) => ({ x: context.surface.width * 0.271, y: context.surface.height * 0.554 })' : "'group-center'"}, + placement: ${spec.family === 'tooltip' ? "'bottom-right'" : "'auto'"}, + offset: ${spec.family === 'tooltip' ? '0' : 'undefined'}, + sort: 'color-domain', + content: ${tooltipContent}, + }, + }) +} + +export const definition = createExampleChart() + +type ExampleDefinition = ReturnType +type ExampleDatum = ExampleDefinition extends DomChartDefinition ? TDatum : never +type ExampleXValue = ExampleDefinition extends DomChartDefinition ? TXValue : never +type ExampleYValue = ExampleDefinition extends DomChartDefinition ? TYValue : never` +} + +function printSpecializedFunction(declaration, spec, isRoot) { + let transformed = declaration + for (let index = 0; index < 3; index += 1) { + transformed = transformFunction(transformed, spec) + } + transformed = removeSpecParameter(transformed) + transformed = removeUnusedFunctionConstants(transformed) + let source = printer.printNode( + ts.EmitHint.Unspecified, + transformed, + implementationFile, + ) + if (isRoot) { + source = source.replace( + /^function\s+[A-Za-z_$][\w$]*/u, + 'function createDefinition', + ) + } + if (spec.family === 'bar' && spec.variant === 'negative') { + source = source.replace( + /\n\s*text:\s*\(row\)\s*=>\s*row\.desktop\.toLocaleString\(["']en-US["']\),/u, + '', + ) + } + for (const name of ['dotMarks', 'labelMarks']) { + const emptyDeclaration = new RegExp( + `\\n?\\s*const\\s+${name}\\s*=\\s*\\[\\];?`, + 'u', + ) + if (!emptyDeclaration.test(source)) continue + source = source + .replace(emptyDeclaration, '') + .replace(new RegExp(`\\.\\.\\.${name},?`, 'gu'), '') + } + source = source.replaceAll(/,?\s*\.\.\.\[\]/gu, '') + return source +} + +function transformFunction(declaration, spec) { + const constants = collectPrimitiveConstants(declaration, spec) + const result = ts.transform(declaration, [ + (context) => { + const visit = (node) => { + if ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'spec' + ) { + return literal(spec[node.name.text]) + } + const visited = ts.visitEachChild(node, visit, context) + if (ts.isIfStatement(visited)) { + const value = evaluate(visited.expression, constants) + if (typeof value === 'boolean') { + const selected = value + ? visited.thenStatement + : visited.elseStatement + if (!selected) return ts.factory.createEmptyStatement() + return ts.isBlock(selected) ? [...selected.statements] : selected + } + } + if (ts.isConditionalExpression(visited)) { + const value = evaluate(visited.condition, constants) + if (typeof value === 'boolean') { + return value ? visited.whenTrue : visited.whenFalse + } + } + if (ts.isBinaryExpression(visited)) { + const value = evaluate(visited, constants) + if (typeof value === 'boolean') return literal(value) + } + if (ts.isArrayLiteralExpression(visited)) { + return ts.factory.updateArrayLiteralExpression( + visited, + visited.elements.flatMap((element) => + ts.isSpreadElement(element) && + ts.isArrayLiteralExpression(element.expression) + ? [...element.expression.elements] + : [element], + ), + ) + } + if (ts.isObjectLiteralExpression(visited)) { + const flattened = visited.properties.flatMap((property) => + ts.isSpreadAssignment(property) && + ts.isObjectLiteralExpression(property.expression) + ? [...property.expression.properties] + : [property], + ) + const lastPropertyIndex = new Map() + flattened.forEach((property, index) => { + if ( + (ts.isPropertyAssignment(property) || + ts.isShorthandPropertyAssignment(property)) && + (ts.isIdentifier(property.name) || + ts.isStringLiteral(property.name)) + ) { + lastPropertyIndex.set(property.name.text, index) + } + }) + return ts.factory.updateObjectLiteralExpression( + visited, + flattened.filter((property, index) => { + if ( + (ts.isPropertyAssignment(property) || + ts.isShorthandPropertyAssignment(property)) && + (ts.isIdentifier(property.name) || + ts.isStringLiteral(property.name)) + ) { + return lastPropertyIndex.get(property.name.text) === index + } + return true + }), + ) + } + if (ts.isBlock(visited)) { + const statements = [] + for (const statement of visited.statements) { + if (ts.isEmptyStatement(statement)) continue + statements.push(statement) + if ( + ts.isReturnStatement(statement) || + ts.isThrowStatement(statement) + ) + break + } + return ts.factory.updateBlock(visited, statements) + } + return visited + } + return (node) => ts.visitNode(node, visit) + }, + ]) + const transformed = result.transformed[0] + result.dispose() + return transformed +} + +function removeSpecParameter(declaration) { + return ts.factory.updateFunctionDeclaration( + declaration, + declaration.modifiers, + declaration.asteriskToken, + declaration.name, + declaration.typeParameters, + declaration.parameters.filter( + (parameter) => + !(ts.isIdentifier(parameter.name) && parameter.name.text === 'spec'), + ), + declaration.type, + declaration.body, + ) +} + +function removeUnusedFunctionConstants(declaration) { + if (!declaration.body) return declaration + let statements = [...declaration.body.statements] + for (let pass = 0; pass < 4; pass += 1) { + const used = identifierNames( + statements + .map((statement) => + printer.printNode( + ts.EmitHint.Unspecified, + statement, + implementationFile, + ), + ) + .join('\n'), + { excludeDeclarations: true }, + ) + statements = statements.filter((statement) => { + if ( + !ts.isVariableStatement(statement) || + statement.declarationList.declarations.length !== 1 + ) { + return true + } + const item = statement.declarationList.declarations[0] + return !ts.isIdentifier(item.name) || used.has(item.name.text) + }) + } + return ts.factory.updateFunctionDeclaration( + declaration, + declaration.modifiers, + declaration.asteriskToken, + declaration.name, + declaration.typeParameters, + declaration.parameters, + declaration.type, + ts.factory.updateBlock(declaration.body, statements), + ) +} + +function collectPrimitiveConstants(declaration, spec) { + const constants = new Map() + const body = declaration.body + if (!body) return constants + for (let pass = 0; pass < 4; pass += 1) { + for (const statement of body.statements) { + if (!ts.isVariableStatement(statement)) continue + for (const item of statement.declarationList.declarations) { + if (!ts.isIdentifier(item.name) || !item.initializer) continue + const value = evaluate(item.initializer, constants, spec) + if ( + typeof value === 'boolean' || + typeof value === 'string' || + typeof value === 'number' + ) { + constants.set(item.name.text, value) + } + } + } + } + return constants +} + +function evaluate(node, constants, spec) { + if (ts.isParenthesizedExpression(node)) { + return evaluate(node.expression, constants, spec) + } + if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text + if (node.kind === ts.SyntaxKind.TrueKeyword) return true + if (node.kind === ts.SyntaxKind.FalseKeyword) return false + if (ts.isIdentifier(node)) return constants.get(node.text) + if ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'spec' + ) { + return spec?.[node.name.text] + } + if ( + ts.isPrefixUnaryExpression(node) && + node.operator === ts.SyntaxKind.ExclamationToken + ) { + const value = evaluate(node.operand, constants, spec) + return typeof value === 'boolean' ? !value : undefined + } + if (ts.isBinaryExpression(node)) { + const left = evaluate(node.left, constants, spec) + const right = evaluate(node.right, constants, spec) + if ( + left !== undefined && + right !== undefined && + node.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken + ) + return left === right + if ( + left !== undefined && + right !== undefined && + node.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken + ) + return left !== right + if (node.operatorToken.kind === ts.SyntaxKind.BarBarToken) { + return typeof left === 'boolean' && typeof right === 'boolean' + ? left || right + : undefined + } + if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + return typeof left === 'boolean' && typeof right === 'boolean' + ? left && right + : undefined + } + } + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.arguments.length === 1 + ) { + const receiver = evaluate(node.expression.expression, constants, spec) + const argument = evaluate(node.arguments[0], constants, spec) + if (typeof receiver === 'string' && typeof argument === 'string') { + if (node.expression.name.text === 'includes') + return receiver.includes(argument) + if (node.expression.name.text === 'startsWith') + return receiver.startsWith(argument) + } + if ( + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'horizontalVariants' && + node.expression.name.text === 'has' && + typeof argument === 'string' + ) { + return horizontalVariants.has(argument) + } + } + return undefined +} + +function literal(value) { + if (typeof value === 'string') return ts.factory.createStringLiteral(value) + if (typeof value === 'number') return ts.factory.createNumericLiteral(value) + if (typeof value === 'boolean') + return value ? ts.factory.createTrue() : ts.factory.createFalse() + return ts.factory.createIdentifier('undefined') +} + +function typeAliasesFor(spec) { + if (spec.variant !== 'interactive') return '' + if (spec.family === 'area') { + return "type InteractiveTimeRange = '90d' | '30d' | '7d'\n\n" + } + if (spec.family === 'bar' || spec.family === 'line') { + return "type InteractiveSeries = 'desktop' | 'mobile'\n\n" + } + return '' +} + +function interactiveState(spec) { + if (spec.variant !== 'interactive') return undefined + if (spec.family === 'area') { + return { + parameter: 'timeRange', + type: 'InteractiveTimeRange', + defaultValue: "'90d'", + } + } + if (spec.family === 'bar' || spec.family === 'line') { + return { + parameter: 'activeSeries', + type: 'InteractiveSeries', + defaultValue: "'desktop'", + } + } + return { parameter: 'activeMonth', type: 'string', defaultValue: "'january'" } +} + +function importsFor(source) { + const usedIdentifiers = identifierNames(source) + const groups = new Map() + const imports = { + react: ['useMemo', 'useRef', 'useState'], + '@tanstack/charts': [ + 'areaY', + 'barX', + 'barY', + 'd3Curve', + 'defineChart', + 'dot', + 'group', + 'lineY', + 'stack', + 'text', + 'type ChartPoint', + 'type ChartValue', + 'type DomChartDefinition', + ], + '@tanstack/charts/polar': [ + 'angleGrid', + 'focusGroupAngle', + 'pie', + 'polar', + 'radialArc', + 'radialArea', + 'radialBarAngle', + 'radialDot', + 'radialGrid', + 'radialRule', + 'radialText', + ], + '@tanstack/charts/react/tooltip': ['RendererChart'], + '@tanstack/charts/tooltip': ['tooltip'], + '@tanstack/charts/motion': ['motion'], + 'd3-scale': ['scaleBand', 'scaleLinear', 'scalePoint'], + 'd3-shape': [ + 'curveLinear', + 'curveLinearClosed', + 'curveMonotoneX', + 'curveNatural', + 'curveStep', + ], + '@charts-poc/demo-data/shadcn': [ + 'shadcnActivities', + 'shadcnBrowsers', + 'shadcnColors', + 'shadcnMonths', + 'shadcnRadarDefault', + 'shadcnRadarFilled', + 'shadcnRadarLines', + 'shadcnRadarMultiple', + 'shadcnSeriesRows', + 'type ShadcnActivityDatum', + 'type ShadcnBrowserDatum', + 'type ShadcnMonthDatum', + 'type ShadcnSeriesDatum', + ], + } + for (const [module, names] of Object.entries(imports)) { + const used = names.filter((entry) => { + const name = entry.replace(/^type\s+/u, '') + return usedIdentifiers.has(name) + }) + if (used.length > 0) groups.set(module, used) + } + if (/\binteractiveAreaData\b/u.test(source)) { + groups.set('@charts-poc/demo-data/shadcn-area-interactive-data', [ + 'default:interactiveAreaData', + ]) + } + return [...groups] + .map(([module, names]) => { + const defaultImport = names.find((name) => name.startsWith('default:')) + const named = names.filter((name) => !name.startsWith('default:')) + if (defaultImport) + return `import ${defaultImport.slice(8)} from '${module}'` + return `import { ${named.join(', ')} } from '${module}'` + }) + .join('\n') +} + +function identifierNames(source, options = {}) { + const file = ts.createSourceFile( + 'generated.tsx', + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ) + const names = new Set() + const visit = (node) => { + if (ts.isIdentifier(node)) { + const parent = node.parent + const declarationName = + (ts.isVariableDeclaration(parent) || + ts.isParameter(parent) || + ts.isFunctionDeclaration(parent)) && + parent.name === node + const propertyName = + (ts.isPropertyAssignment(parent) || + ts.isMethodDeclaration(parent) || + ts.isPropertyAccessExpression(parent)) && + parent.name === node + if ((!options.excludeDeclarations || !declarationName) && !propertyName) { + names.add(node.text) + } + } + ts.forEachChild(node, visit) + } + visit(file) + return names +} + +function componentFor(spec) { + const interactive = spec.variant === 'interactive' + const state = interactiveState(spec) + const stateSetup = !state + ? '' + : spec.family === 'area' + ? "const [timeRange, setTimeRange] = useState('90d')" + : spec.family === 'bar' || spec.family === 'line' + ? "const [activeSeries, setActiveSeries] = useState('desktop')" + : "const [activeMonth, setActiveMonth] = useState('january')" + const stateValue = state?.parameter + const chartDefinition = state + ? `useMemo(() => createExampleChart(${stateValue}), [${stateValue}])` + : 'definition' + const headerAction = !interactive + ? 'null' + : spec.family === 'area' + ? ` setTimeRange(value as InteractiveTimeRange)} options={[{ value: '90d', label: 'Last 3 months' }, { value: '30d', label: 'Last 30 days' }, { value: '7d', label: 'Last 7 days' }]} />` + : spec.family === 'bar' || spec.family === 'line' + ? '' + : ` ({ value: row.browser, label: titleCase(row.browser), swatch: shadcnColors[index] }))} />` + const square = spec.square + const variant = interactive ? `interactive-${spec.family}` : 'default' + const footer = + spec.family === 'tooltip' || + interactive || + (spec.family === 'pie' && spec.variant === 'legend') + ? 'null' + : `` + const legend = spec.legend ? '' : 'null' + const chartProps = + spec.family === 'tooltip' + ? `onRender={({ scene, interaction }) => { + if (seededTooltip.current) return + const point = scene.points.find((candidate) => (candidate.datum as ShadcnActivityDatum).date === '2024-07-16') + if (!point) return + seededTooltip.current = true + interaction.setControlledFocus(point, { source: 'programmatic' }) + }} + renderTooltipBody={({ points }) => []} variant=${JSON.stringify(spec.variant)} />}` + : '' + const tooltipRef = + spec.family === 'tooltip' ? 'const seededTooltip = useRef(false)' : '' + const helperSource = componentHelpers(spec) + + return `export interface ExampleProps { width?: number; height?: number } + +export default function Example({ width = 640, height = 600 }: ExampleProps) { + ${stateSetup} + ${tooltipRef} + const chartDefinition = ${chartDefinition} + const renderer = useMemo(() => motion({ initial: 'always', transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 } }), []) + const contentWidth = Math.max(1, width - 50) + const chartWidth = ${square ? `Math.min(${interactive && spec.family === 'pie' ? 300 : 250}, contentWidth)` : 'contentWidth'} + const chartHeight = ${ + square + ? 'chartWidth' + : interactive && ['area', 'bar', 'line'].includes(spec.family) + ? '250' + : spec.legend + ? '(contentWidth * 9) / 16 - 27' + : '(contentWidth * 9) / 16' + } + const headerAction = ${headerAction} + const legend = ${legend} + const footer = ${footer} + + return ( +
+
+
+

${escapeJsx(spec.title)}

${escapeJsx(spec.description)}

+ {headerAction ?
{headerAction}
: null} +
+
+
+ +
+ {legend ?
{legend}
: null} +
+ {footer ?
{footer}
: null} +
+
+ ) +} + +${helperSource}` +} + +function componentHelpers(spec) { + const helpers = [] + if (spec.legend) { + const labels = + spec.family === 'pie' || spec.family === 'radial' + ? "['chrome', 'safari', 'firefox', 'edge', 'other']" + : "['desktop', 'mobile']" + helpers.push(`function Legend() { + return <>{${labels}.map((label, index) => ${spec.variant === 'icons' ? '' : ''}{titleCase(label)})} +}`) + } + if ( + spec.family !== 'tooltip' && + spec.variant !== 'interactive' && + !(spec.family === 'pie' && spec.variant === 'legend') + ) { + helpers.push(`function TrendFooter({ note }: { note: string }) { + return <>
Trending up by 5.2% this month
{note}
+}`) + } + if ( + spec.variant === 'interactive' && + (spec.family === 'area' || spec.family === 'pie') + ) { + helpers.push(`function SelectControl({ value, options, onChange }: { value: string; options: readonly { value: string; label: string; swatch?: string }[]; onChange: (value: string) => void }) { + const selected = options.find((option) => option.value === value) + return +}`) + } + if ( + spec.variant === 'interactive' && + (spec.family === 'bar' || spec.family === 'line') + ) { + helpers.push(`function BarMetrics({ active, onChange }: { active: InteractiveSeries; onChange: (series: InteractiveSeries) => void }) { + const totals = { desktop: interactiveBarRows.reduce((sum, row) => sum + row.desktop, 0), mobile: interactiveBarRows.reduce((sum, row) => sum + row.mobile, 0) } + return <>{twoSeries.map((series) => )} +}`) + } + return helpers.join('\n\n') +} + +function escapeJsx(value) { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') +} diff --git a/scripts/typescript-source.mjs b/scripts/typescript-source.mjs new file mode 100644 index 00000000..81f3ec60 --- /dev/null +++ b/scripts/typescript-source.mjs @@ -0,0 +1,110 @@ +import fs from 'node:fs' +import path from 'node:path' +import ts from 'typescript' + +const unusedDiagnosticCodes = new Set([6133, 6192, 6196, 6198]) + +const compilerOptions = { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + jsx: ts.JsxEmit.ReactJSX, + strict: true, + noEmit: true, + noUnusedLocals: true, + skipLibCheck: true, + allowImportingTsExtensions: true, + allowArbitraryExtensions: true, + resolveJsonModule: true, +} + +export function removeUnusedTypeScript(source, filePath) { + const normalizedPath = path.resolve(filePath) + let result = source + + for (let pass = 0; pass < 8; pass += 1) { + const service = createLanguageService(normalizedPath, result, pass) + try { + const textChanges = service + .getSemanticDiagnostics(normalizedPath) + .filter((diagnostic) => unusedDiagnosticCodes.has(diagnostic.code)) + .filter( + (diagnostic) => + !diagnosticTargetsParameter( + service.getProgram(), + normalizedPath, + diagnostic.start ?? 0, + ), + ) + .flatMap((diagnostic) => + service.getCodeFixesAtPosition( + normalizedPath, + diagnostic.start ?? 0, + (diagnostic.start ?? 0) + (diagnostic.length ?? 0), + [diagnostic.code], + {}, + {}, + ), + ) + .filter((fix) => fix.fixName === 'unusedIdentifier') + .flatMap((fix) => fix.changes) + .filter((change) => path.resolve(change.fileName) === normalizedPath) + .flatMap((change) => change.textChanges) + .filter( + (change, index, all) => + all.findIndex( + (candidate) => + candidate.span.start === change.span.start && + candidate.span.length === change.span.length && + candidate.newText === change.newText, + ) === index, + ) + .sort((left, right) => right.span.start - left.span.start) + if (textChanges.length === 0) return result + for (const change of textChanges) { + result = + result.slice(0, change.span.start) + + change.newText + + result.slice(change.span.start + change.span.length) + } + } finally { + service.dispose() + } + } + + throw new Error(`unused TypeScript cleanup did not converge for ${filePath}`) +} + +function diagnosticTargetsParameter(program, filePath, position) { + const sourceFile = program?.getSourceFile(filePath) + if (!sourceFile) return false + let node = ts.getTokenAtPosition(sourceFile, position) + while (node && node !== sourceFile) { + if (ts.isParameter(node)) return true + node = node.parent + } + return false +} + +function createLanguageService(filePath, source, version) { + const host = { + getScriptFileNames: () => [filePath], + getScriptVersion: () => String(version), + getScriptSnapshot(candidate) { + if (path.resolve(candidate) === filePath) { + return ts.ScriptSnapshot.fromString(source) + } + if (!fs.existsSync(candidate)) return undefined + return ts.ScriptSnapshot.fromString(fs.readFileSync(candidate, 'utf8')) + }, + getCurrentDirectory: () => process.cwd(), + getCompilationSettings: () => compilerOptions, + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + } + return ts.createLanguageService(host) +} From 298202a15c1a39bd5b1a198bbc07680081e47266 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 14 Aug 2026 14:08:00 -0600 Subject: [PATCH 2/2] Sync catalog documentation navigation --- docs/config.json | 10 +++++----- packages/charts-core/docs/config.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/config.json b/docs/config.json index b7e4b544..72d0734a 100644 --- a/docs/config.json +++ b/docs/config.json @@ -777,23 +777,23 @@ "to": "/charts/catalog/charts/127-shadcn-dashboard/" }, { - "label": "shadcn bar chart – multiple", + "label": "shadcn Bar Chart - Multiple", "to": "/charts/catalog/charts/128-shadcn-bar-multiple/" }, { - "label": "shadcn pie chart – donut with text", + "label": "shadcn Pie Chart - Donut with Text", "to": "/charts/catalog/charts/129-shadcn-pie-donut-text/" }, { - "label": "shadcn radar chart – multiple", + "label": "shadcn Radar Chart - Multiple", "to": "/charts/catalog/charts/130-shadcn-radar-multiple/" }, { - "label": "shadcn radial chart – text", + "label": "shadcn Radial Chart - Text", "to": "/charts/catalog/charts/131-shadcn-radial-text/" }, { - "label": "shadcn tooltip – advanced", + "label": "shadcn Tooltip - Advanced", "to": "/charts/catalog/charts/132-shadcn-tooltip-advanced/" }, { diff --git a/packages/charts-core/docs/config.json b/packages/charts-core/docs/config.json index b7e4b544..72d0734a 100644 --- a/packages/charts-core/docs/config.json +++ b/packages/charts-core/docs/config.json @@ -777,23 +777,23 @@ "to": "/charts/catalog/charts/127-shadcn-dashboard/" }, { - "label": "shadcn bar chart – multiple", + "label": "shadcn Bar Chart - Multiple", "to": "/charts/catalog/charts/128-shadcn-bar-multiple/" }, { - "label": "shadcn pie chart – donut with text", + "label": "shadcn Pie Chart - Donut with Text", "to": "/charts/catalog/charts/129-shadcn-pie-donut-text/" }, { - "label": "shadcn radar chart – multiple", + "label": "shadcn Radar Chart - Multiple", "to": "/charts/catalog/charts/130-shadcn-radar-multiple/" }, { - "label": "shadcn radial chart – text", + "label": "shadcn Radial Chart - Text", "to": "/charts/catalog/charts/131-shadcn-radial-text/" }, { - "label": "shadcn tooltip – advanced", + "label": "shadcn Tooltip - Advanced", "to": "/charts/catalog/charts/132-shadcn-tooltip-advanced/" }, {