From e717986acb297797150761335449f7c4b1a7da64 Mon Sep 17 00:00:00 2001 From: iexitdev Date: Thu, 28 May 2026 21:54:53 -0400 Subject: [PATCH 1/7] Polish chart docs site --- apps/site/astro.config.mjs | 6 +- apps/site/src/components/ChartsSupported.tsx | 261 ++++++++++++++- apps/site/src/components/Head.astro | 50 +++ apps/site/src/styles/starlight.css | 325 +++++++++++++++++++ docs/README.md | 6 +- docs/charts/area.md | 109 +++++++ docs/charts/{pie-and-donut.md => donut.md} | 74 +---- docs/charts/{line-and-area.md => line.md} | 89 ++--- docs/charts/pie.md | 96 ++++++ 9 files changed, 879 insertions(+), 137 deletions(-) create mode 100644 docs/charts/area.md rename docs/charts/{pie-and-donut.md => donut.md} (50%) rename docs/charts/{line-and-area.md => line.md} (70%) create mode 100644 docs/charts/pie.md diff --git a/apps/site/astro.config.mjs b/apps/site/astro.config.mjs index cce9601a..a7f8baa4 100644 --- a/apps/site/astro.config.mjs +++ b/apps/site/astro.config.mjs @@ -82,9 +82,11 @@ export default defineConfig({ { label: "Charts", items: [ - { slug: "docs/charts/line-and-area" }, + { slug: "docs/charts/line" }, + { slug: "docs/charts/area" }, { slug: "docs/charts/bar" }, - { slug: "docs/charts/pie-and-donut" }, + { slug: "docs/charts/pie" }, + { slug: "docs/charts/donut" }, { slug: "docs/charts/progress" }, { slug: "docs/charts/contribution-heatmap" } ] diff --git a/apps/site/src/components/ChartsSupported.tsx b/apps/site/src/components/ChartsSupported.tsx index ebcf19fd..62ae1fcf 100644 --- a/apps/site/src/components/ChartsSupported.tsx +++ b/apps/site/src/components/ChartsSupported.tsx @@ -10,10 +10,15 @@ type ChartKind = | "donut" | "progress" | "heatmap" + | "radar" + | "combined" + | "candlestick" | "more"; type ChartType = { + docsHref?: string; kind: ChartKind; + pro?: boolean; title: string; subtitle?: string; }; @@ -21,14 +26,29 @@ type ChartType = { type ThemeMode = "dark" | "light"; const chartTypes: ChartType[] = [ - { kind: "line", title: "Line Chart" }, - { kind: "area", title: "Area Chart" }, - { kind: "bar", title: "Bar Chart" }, - { kind: "stackedBar", title: "Stacked Bar Chart" }, - { kind: "pie", title: "Pie Chart" }, - { kind: "donut", title: "Donut Chart" }, - { kind: "progress", title: "Progress Circle" }, - { kind: "heatmap", title: "Contribution Heatmap" }, + { docsHref: "/docs/charts/line/", kind: "line", title: "Line Chart" }, + { docsHref: "/docs/charts/area/", kind: "area", title: "Area Chart" }, + { docsHref: "/docs/charts/bar/", kind: "bar", title: "Bar Chart" }, + { + docsHref: "/docs/charts/bar/#stacked-bars", + kind: "stackedBar", + title: "Stacked Bar Chart" + }, + { docsHref: "/docs/charts/pie/", kind: "pie", title: "Pie Chart" }, + { docsHref: "/docs/charts/donut/", kind: "donut", title: "Donut Chart" }, + { + docsHref: "/docs/charts/progress/", + kind: "progress", + title: "Progress Circle" + }, + { + docsHref: "/docs/charts/contribution-heatmap/", + kind: "heatmap", + title: "Contribution Heatmap" + }, + { kind: "radar", pro: true, subtitle: "planned", title: "Radar Chart" }, + { kind: "combined", pro: true, title: "Combined Chart" }, + { kind: "candlestick", pro: true, title: "Candlestick Chart" }, { kind: "more", title: "More charts", subtitle: "coming soon" } ]; @@ -167,6 +187,15 @@ const getThemeStyles = (mode: ThemeMode) => { label: { color: isLight ? "rgba(0, 0, 0, 0.68)" : "rgba(255, 255, 255, 0.72)" }, + pro: { + backgroundColor: isLight + ? "rgba(0, 0, 0, 0.045)" + : "rgba(255, 255, 255, 0.075)", + borderColor: isLight + ? "rgba(0, 0, 0, 0.12)" + : "rgba(255, 255, 255, 0.14)", + color: isLight ? "rgba(0, 0, 0, 0.62)" : "rgba(255, 255, 255, 0.76)" + }, separator: { background: isLight ? "linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.09) 20%, rgba(0, 0, 0, 0.09) 80%, transparent)" @@ -608,6 +637,175 @@ const ChartArtwork = ({ ); + case "radar": + return ( + + + + + + + + + + + + + {[ + [120, 39, 120, 32], + [153, 66, 160, 72], + [139, 106, 130, 111], + [98, 94, 96, 86], + [88, 62, 82, 67] + ].map(([baseX, baseY, hoverX, hoverY], index) => { + const localProgress = staggerProgress(progress, index, 0.04); + + return ( + + ); + })} + + ); + case "combined": + return ( + + + + + + + + + {[46, 72, 54, 86, 68].map((baseHeight, index) => { + const localProgress = staggerProgress(progress, index, 0.045); + const height = lerp( + baseHeight, + [68, 52, 74, 62, 94][index], + localProgress + ); + + return ( + + ); + })} + + + + + ); + case "candlestick": + return ( + + + {[ + [52, 48, 116, 60, 120], + [82, 36, 104, 58, 118], + [112, 56, 126, 42, 116], + [142, 42, 108, 50, 114], + [172, 60, 118, 48, 108], + [202, 34, 94, 62, 112] + ].map(([x, y1, y2, activeY1, activeY2], index) => { + const localProgress = staggerProgress(progress, index, 0.035); + + return ( + + ); + })} + + + {[ + [43, 72, 26, 82, 24, 0.34, 0.26], + [73, 52, 38, 64, 30, 0.9, 0.74], + [103, 82, 28, 60, 40, 0.34, 0.42], + [133, 58, 32, 42, 48, 0.9, 0.95], + [163, 82, 24, 50, 44, 0.34, 0.48], + [193, 48, 30, 70, 28, 0.9, 0.62] + ].map( + ( + [x, y, height, activeY, activeHeight, opacity, activeOpacity], + index + ) => { + const localProgress = staggerProgress(progress, index, 0.04); + + return ( + + ); + } + )} + + + ); case "more": return ( @@ -701,13 +899,17 @@ const ChartTile = ({ enterDuration, exitDuration ); + const tileClassName = + "group relative block min-w-0 text-current no-underline outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-4px] focus-visible:outline-white/45 [html[data-theme='light']_&]:focus-visible:outline-black/40"; + const tileEvents = { + onBlur: () => setActive(false), + onFocus: () => setActive(true), + onPointerEnter: () => setActive(true), + onPointerLeave: () => setActive(false) + }; - return ( -
setActive(true)} - onPointerLeave={() => setActive(false)} - > + const content = ( + <> {index % 2 === 0 && index < chartTypes.length - 1 && (
+ + ); + + if (!chart.docsHref) { + return ( +
+ {content} +
+ ); + } + + return ( + + {content} + ); }; @@ -811,7 +1040,7 @@ export default function ChartsSupported() {
Read docs diff --git a/apps/site/src/components/Head.astro b/apps/site/src/components/Head.astro index eab32df5..4a8d4cd7 100644 --- a/apps/site/src/components/Head.astro +++ b/apps/site/src/components/Head.astro @@ -1,4 +1,6 @@ --- +import ThemeInit from "./ThemeInit.astro"; + const { head } = Astro.locals.starlightRoute; const isDev = import.meta.env.DEV; const reactRefreshPreamble = ` @@ -10,6 +12,54 @@ const reactRefreshPreamble = ` `; --- + + {head.map(({ tag: Tag, attrs, content }) => )} { isDev ? ( diff --git a/apps/site/src/styles/starlight.css b/apps/site/src/styles/starlight.css index b04a4952..025f299c 100644 --- a/apps/site/src/styles/starlight.css +++ b/apps/site/src/styles/starlight.css @@ -7,3 +7,328 @@ border-top-left-radius: calc(var(--ec-brdRad) + var(--ec-brdWd)); border-top-right-radius: calc(var(--ec-brdRad) + var(--ec-brdWd)); } + +:root { + --sl-font: "Inter", system-ui, sans-serif; + --sl-content-width: 47rem; + --sl-sidebar-width: 18rem; + --sl-content-pad-x: 1.25rem; + --sl-content-gap-y: 1.15rem; + --sl-line-height: 1.72; + --sl-line-height-headings: 1.08; + --sl-text-h1: 3.25rem; + --sl-text-h2: 2.05rem; + --sl-text-h3: 1.45rem; + --sl-color-accent-low: #13213a; + --sl-color-accent: #78a8ff; + --sl-color-accent-high: #d8e6ff; + --sl-color-text-accent: #d8e6ff; + --sl-color-text: rgba(255, 255, 255, 0.68); + --sl-color-white: #ffffff; + --sl-color-gray-1: rgba(255, 255, 255, 0.9); + --sl-color-gray-2: rgba(255, 255, 255, 0.68); + --sl-color-gray-3: rgba(255, 255, 255, 0.46); + --sl-color-gray-4: rgba(255, 255, 255, 0.28); + --sl-color-gray-5: rgba(255, 255, 255, 0.14); + --sl-color-gray-6: #171a21; + --sl-color-black: #08090d; + --sl-color-bg: #0b0c11; + --sl-color-bg-nav: #08090d; + --sl-color-bg-sidebar: #11131a; + --sl-color-bg-inline-code: rgba(255, 255, 255, 0.09); + --sl-color-hairline: rgba(255, 255, 255, 0.1); + --sl-color-hairline-light: rgba(255, 255, 255, 0.1); + --sl-color-hairline-shade: rgba(255, 255, 255, 0.1); + --sl-shadow-sm: none; + --sl-shadow-md: none; + --sl-shadow-lg: none; +} + +:root[data-theme="light"] { + --sl-color-accent-low: #eaf2ff; + --sl-color-accent: #2563eb; + --sl-color-accent-high: #0f3a78; + --sl-color-text-accent: #0f3a78; + --sl-color-text: rgba(7, 23, 51, 0.7); + --sl-color-white: #071733; + --sl-color-gray-1: rgba(7, 23, 51, 0.88); + --sl-color-gray-2: rgba(7, 23, 51, 0.68); + --sl-color-gray-3: rgba(7, 23, 51, 0.48); + --sl-color-gray-4: rgba(7, 23, 51, 0.28); + --sl-color-gray-5: rgba(7, 23, 51, 0.12); + --sl-color-gray-6: #edf3fb; + --sl-color-gray-7: #f8fbff; + --sl-color-black: #ffffff; + --sl-color-bg: #f6f9fd; + --sl-color-bg-nav: #ffffff; + --sl-color-bg-sidebar: #ffffff; + --sl-color-bg-inline-code: rgba(7, 23, 51, 0.07); + --sl-color-hairline: rgba(7, 23, 51, 0.1); + --sl-color-hairline-light: rgba(7, 23, 51, 0.1); + --sl-color-hairline-shade: rgba(7, 23, 51, 0.1); + --sl-shadow-sm: none; + --sl-shadow-md: none; + --sl-shadow-lg: none; +} + +html { + background: var(--sl-color-bg); +} + +body { + background: var(--sl-color-bg); +} + +.site-title { + color: var(--sl-color-white); + gap: 0.75rem; + font-size: 0.94rem; + font-weight: 700; +} + +.site-title::before { + display: grid; + width: 2rem; + height: 2rem; + flex: 0 0 auto; + place-items: center; + border-radius: 0.5rem; + background: #ffffff; + color: #05060a; + content: "CK"; + font-size: 0.82rem; + font-weight: 800; +} + +:root[data-theme="light"] .site-title::before { + background: #071733; + color: #ffffff; +} + +button[data-open-modal] { + border-radius: 0.75rem; +} + +@media (min-width: 50rem) { + button[data-open-modal] { + border-color: var(--sl-color-hairline); + background-color: rgba(255, 255, 255, 0.035); + max-width: 20rem; + } + + :root[data-theme="light"] button[data-open-modal] { + background-color: rgba(7, 23, 51, 0.035); + } +} + +.social-icons::after { + border-color: var(--sl-color-hairline); +} + +@media (min-width: 50rem) { + .sidebar-pane { + background: var(--sl-color-bg-sidebar); + } + + :root[data-theme="light"] .sidebar-pane { + background: var(--sl-color-bg-sidebar); + } +} + +.sidebar-content { + padding-top: 1.25rem; +} + +.sidebar-content .large { + color: var(--sl-color-white); + font-size: 0.9rem; + font-weight: 700; +} + +.sidebar-content summary { + border-radius: 0.65rem; + padding: 0.45rem 0.55rem; +} + +.sidebar-content summary:hover { + background: rgba(255, 255, 255, 0.045); +} + +:root[data-theme="light"] .sidebar-content summary:hover { + background: rgba(7, 23, 51, 0.045); +} + +.sidebar-content a { + display: flex; + min-height: 2rem; + align-items: center; + border-radius: 0.55rem; + color: var(--sl-color-gray-2); + font-size: 0.83rem; + font-weight: 650; + line-height: 1.25; + padding: 0.45rem 0.55rem; +} + +.sidebar-content a span { + font-size: inherit; + font-weight: inherit; + line-height: inherit; +} + +.sidebar-content a:hover, +.sidebar-content a:focus { + background: rgba(255, 255, 255, 0.045); + color: var(--sl-color-white); +} + +:root[data-theme="light"] .sidebar-content a:hover, +:root[data-theme="light"] .sidebar-content a:focus { + background: rgba(7, 23, 51, 0.045); +} + +.sidebar-content [aria-current="page"], +.sidebar-content [aria-current="page"]:hover, +.sidebar-content [aria-current="page"]:focus { + background: rgba(120, 168, 255, 0.14); + color: #d8e6ff; + font-size: 0.83rem; + font-weight: 650; + line-height: 1.25; +} + +:root[data-theme="light"] .sidebar-content [aria-current="page"], +:root[data-theme="light"] .sidebar-content [aria-current="page"]:hover, +:root[data-theme="light"] .sidebar-content [aria-current="page"]:focus { + background: #eaf2ff; + color: #0f3a78; +} + +.main-frame { + background: transparent; +} + +.content-panel { + border-color: var(--sl-color-hairline); +} + +.content-panel:first-child { + padding-top: 2.65rem; +} + +.sl-markdown-content { + color: var(--sl-color-text); +} + +.sl-markdown-content h1:not(:where(.not-content *)) { + max-width: 16ch; + font-size: 3.1rem; + font-weight: 760; +} + +.sl-markdown-content h2:not(:where(.not-content *)) { + padding-top: 0.35rem; + font-size: 2rem; + font-weight: 760; +} + +.sl-markdown-content :is(h3, h4):not(:where(.not-content *)) { + font-weight: 730; +} + +.sl-markdown-content p:not(:where(.not-content *)), +.sl-markdown-content li:not(:where(.not-content *)) { + font-size: 0.98rem; +} + +.sl-markdown-content a:not(:where(.not-content *)) { + color: #9ec2ff; + font-weight: 650; + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; +} + +:root[data-theme="light"] .sl-markdown-content a:not(:where(.not-content *)) { + color: #0f54c3; +} + +.sl-markdown-content code:not(:where(.not-content *)) { + border: 1px solid var(--sl-color-hairline); + border-radius: 0.38rem; + color: var(--sl-color-white); +} + +.sl-markdown-content pre:not(:where(.not-content *)), +.expressive-code figure.frame { + border-color: var(--sl-color-hairline); + border-radius: 0.8rem; + background: #1a1d25; +} + +:root[data-theme="light"] .sl-markdown-content pre:not(:where(.not-content *)), +:root[data-theme="light"] .expressive-code figure.frame { + background: #ffffff; +} + +.expressive-code figure.frame > pre { + border-radius: 0.8rem; +} + +.right-sidebar { + border-color: var(--sl-color-hairline); +} + +.right-sidebar-panel { + color: var(--sl-color-gray-3); +} + +.right-sidebar-panel h2 { + font-size: 0.9rem; +} + +.right-sidebar-panel a { + border-radius: 0.5rem; + padding: 0.22rem 0.4rem; +} + +.right-sidebar-panel a:hover { + background: rgba(255, 255, 255, 0.045); +} + +:root[data-theme="light"] .right-sidebar-panel a:hover { + background: rgba(7, 23, 51, 0.045); +} + +.pagination-links a { + border-color: var(--sl-color-hairline); + border-radius: 0.9rem; + background: rgba(255, 255, 255, 0.035); +} + +:root[data-theme="light"] .pagination-links a { + background: #ffffff; +} + +.sl-markdown-content blockquote:not(:where(.not-content *)), +.sl-markdown-content details:not(:where(.not-content *)) { + border-color: var(--sl-color-hairline); +} + +@media (max-width: 49.99rem) { + .site-title { + font-size: 0.86rem; + } + + .site-title::before { + width: 1.85rem; + height: 1.85rem; + } + + .sl-markdown-content h1:not(:where(.not-content *)) { + font-size: 2.2rem; + } + + .sl-markdown-content h2:not(:where(.not-content *)) { + font-size: 1.6rem; + } +} diff --git a/docs/README.md b/docs/README.md index 6cba3a10..d5460d1b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,9 +22,11 @@ Modern v2 examples import from `react-native-chart-kit/v2`. ## Charts -- [Line and area](charts/line-and-area.md) +- [Line](charts/line.md) +- [Area](charts/area.md) - [Bar](charts/bar.md) -- [Pie and donut](charts/pie-and-donut.md) +- [Pie](charts/pie.md) +- [Donut](charts/donut.md) - [Progress](charts/progress.md) - [Contribution heatmap](charts/contribution-heatmap.md) diff --git a/docs/charts/area.md b/docs/charts/area.md new file mode 100644 index 00000000..b8891477 --- /dev/null +++ b/docs/charts/area.md @@ -0,0 +1,109 @@ +--- +title: Area Chart +description: Build filled trend charts with AreaChart or LineChart area mode. +--- + +# Area Chart + +`AreaChart` is the dedicated v2 surface for filled trend charts. It shares the +same data model and interaction primitives as `LineChart`, but defaults to an +area fill. + +## Basic Area + +```tsx +import { AreaChart } from "react-native-chart-kit/v2"; + +const data = [ + { date: "Jan", price: 18 }, + { date: "Feb", price: 34 }, + { date: "Mar", price: 29 }, + { date: "Apr", price: 52 } +]; + +export function PipelineChart() { + return ( + + ); +} +``` + +::chart-preview{id="line-area"} + +## Area Fill + +Use `areaFill` to tune fill opacity. You can also pass `area` to `LineChart` +when a product uses line and area variants from the same component wrapper. + +```tsx + +``` + +Use `areaFill` on the chart for a shared fill style, or per series when each +series needs its own colors or opacity. + +## Threshold Area + +Threshold coloring clips the rendered path and area fill above or below a y +value. Raw points are unchanged. + +```tsx + +``` + +## Dense Area Charts + +For long time series, use the same viewport, scrolling, and decimation options +as `LineChart`. + +```tsx + +``` diff --git a/docs/charts/pie-and-donut.md b/docs/charts/donut.md similarity index 50% rename from docs/charts/pie-and-donut.md rename to docs/charts/donut.md index 16b2c4f6..19069f1a 100644 --- a/docs/charts/pie-and-donut.md +++ b/docs/charts/donut.md @@ -1,45 +1,14 @@ --- -title: Pie and Donut Charts -description: Render segmented pie and donut charts with accessible legends and labels. +title: Donut Chart +description: Render donut charts with center labels, legends, and tap selection. --- -# Pie and Donut Charts +# Donut Chart -`PieChart` and `DonutChart` use the same normalized slice model and -renderer-agnostic arc geometry. +`DonutChart` uses the same normalized slice model as `PieChart`, with a default +inner radius and center-label support. -## Pie Chart - -```tsx -import { PieChart } from "react-native-chart-kit/v2"; - -const data = [ - { channel: "Organic search", share: 42 }, - { channel: "Paid social", share: 24 }, - { channel: "Referrals", share: 18 }, - { channel: "Partners", share: 10 }, - { channel: "Lifecycle", share: 6 } -]; - -export function AcquisitionShare() { - return ( - - ); -} -``` - -::chart-preview{id="pie-basic"} - -## Donut Chart - -Use `DonutChart` for the default donut radius, or pass `innerRadius` / `innerRadiusRatio` directly to `PieChart`. +## Basic Donut ```tsx import { DonutChart } from "react-native-chart-kit/v2"; @@ -59,7 +28,7 @@ import { DonutChart } from "react-native-chart-kit/v2"; ## Current Scope -The first v2 slice supports: +The first v2 donut chart supports: - modern object-row data - theme and preset colors @@ -68,13 +37,13 @@ The first v2 slice supports: - donut center text - rich custom center labels - custom legend item rendering -- external arc labels with connector lines - tap selection with active-slice highlighting - zero and invalid slices without broken paths ## Tap Selection -Use `interaction="tap"` for uncontrolled selection, or pass `selectedIndex` with `interaction.onSelect` for controlled product UI. +Use `interaction="tap"` for uncontrolled selection, or pass `selectedIndex` with +`interaction.onSelect` for controlled product UI. ```tsx const [selectedIndex, setSelectedIndex] = useState(0); @@ -95,9 +64,10 @@ const [selectedIndex, setSelectedIndex] = useState(0); />; ``` -## Custom Legend And Center Label +## Custom Legend and Center Label -Use `legend.renderItem` when the default compact legend is not enough. `centerLabel` can return React content for multi-line KPI labels. +Use `legend.renderItem` when the default compact legend is not enough. +`centerLabel` can return React content for multi-line KPI labels. ```tsx ``` - -## External Arc Labels - -Use `arcLabels` when the chart should explain itself without a separate legend. Small slices are filtered by `minPercentage` so long-tail labels do not collide with the primary categories. - -```tsx - - `${label.split(" ")[0]} ${percentageLabel}` - }} - width={360} - height={260} -/> -``` diff --git a/docs/charts/line-and-area.md b/docs/charts/line.md similarity index 70% rename from docs/charts/line-and-area.md rename to docs/charts/line.md index d94094fd..99e23c20 100644 --- a/docs/charts/line-and-area.md +++ b/docs/charts/line.md @@ -1,14 +1,15 @@ --- -title: Line and Area Charts -description: Build line and area charts with object-row data, renderers, gestures, and themes. +title: Line Chart +description: Build line charts with object-row data, renderers, gestures, and themes. --- -# Line and Area Charts +# Line Chart -`LineChart` is the primary modern v2 chart surface. It uses object-row data, +`LineChart` is the primary modern v2 trend surface. It uses object-row data, explicit keys, renderer-agnostic geometry, and SVG rendering by default. -Use this API for new apps. The legacy `react-native-chart-kit` data shape is handled separately by the compatibility facade. +Use this API for new apps. The legacy `react-native-chart-kit` data shape is +handled separately by the compatibility facade. ## Basic Line @@ -39,7 +40,8 @@ export function RevenueChart() { ## Multi-Series -Use `series` when each line needs its own label, color, marker, curve, or stroke style. +Use `series` when each line needs its own label, color, marker, curve, or +stroke style. ```tsx -``` - -::chart-preview{id="line-area"} - -Use `areaFill` on the chart for a shared fill style, or per series when each series needs its own gradient colors or opacity. - ## Styling Lines and Dots Supported curve values are `linear`, `monotone`, and `step`. @@ -122,11 +103,11 @@ Supported curve values are `linear`, `monotone`, and `step`. ## Threshold Coloring -Threshold coloring clips the rendered path and area fill above or below a y value. Raw points are unchanged. +Threshold coloring clips the rendered path above or below a y value. Raw points +are unchanged. ```tsx ``` -Use a controlled viewport for direct pan, pinch zoom, or a mini-chart range selector. +Use a controlled viewport for direct pan, pinch zoom, or a mini-chart range +selector. ```tsx const [viewport, setViewport] = useState({ @@ -271,13 +253,13 @@ const [viewport, setViewport] = useState({ />; ``` -`yAxisLabelWidth="stable"` reserves label width from the full dataset, so changing the viewport does not make the plot jump when labels change from values such as `$10k` to `$100k`. - -The range selector is composable through `renderLine`, `renderHandle`, and `renderWindow`, so products can brand the overview path, handles, and selected window without replacing the built-in viewport logic. +`yAxisLabelWidth="stable"` reserves label width from the full dataset, so +changing the viewport does not make the plot jump when labels change. ## Reference Overlays -Reference lines and bands are clipped to the plot bounds. Line labels default to automatic vertical placement and try to avoid nearby series geometry. +Reference lines and bands are clipped to the plot bounds. Line labels default to +automatic vertical placement and try to avoid nearby series geometry. ```tsx ``` -Set `labelPlacement="above"` or `labelPlacement="below"` only when you need a fixed position. - ## Layout Debug -Use `debugLayout` in development when a chart clips labels, legends, or tooltips. The overlay draws the outer bounds, plot bounds, visible label boxes, legend boxes, and tooltip box. +Use `debugLayout` in development when a chart clips labels, legends, or tooltips. ```tsx ``` -`onLayoutDebug` receives the same rectangle model that the overlay renders, so bug reports can include structured layout evidence alongside screenshots. - ## Labels and Axes Useful label props: @@ -324,11 +302,11 @@ Useful label props: - `axisLabelAnimation`: crossfade y-axis label changes during viewport changes. - `yAxisLabelWidth`: `auto`, `stable`, or a fixed number. -When `labelStrategy` is `auto` or `skip`, duplicate formatted x-axis labels are collapsed before collision solving. Use `labelStrategy="show"` when repeated labels are intentional. - ## Decimation -LineChart uses automatic path-only min/max decimation by default. This reduces SVG path complexity for dense charts while preserving source points for selection, tooltips, labels, and custom dots. +LineChart uses automatic path-only min/max decimation by default. This reduces +SVG path complexity for dense charts while preserving source points for +selection, tooltips, labels, and custom dots. ```tsx + ); +} +``` + +::chart-preview{id="pie-basic"} + +## Current Scope + +The first v2 pie chart supports: + +- modern object-row data +- theme and preset colors +- bottom wrapped legend +- percentage labels in the legend +- custom legend item rendering +- external arc labels with connector lines +- tap selection with active-slice highlighting +- zero and invalid slices without broken paths + +## Tap Selection + +Use `interaction="tap"` for uncontrolled selection, or pass `selectedIndex` with +`interaction.onSelect` for controlled product UI. + +```tsx +const [selectedIndex, setSelectedIndex] = useState(0); + + setSelectedIndex(event.index) + }} + activeSlice={{ inactiveOpacity: 0.36, strokeWidth: 4 }} + width={360} + height={260} +/>; +``` + +## External Arc Labels + +Use `arcLabels` when the chart should explain itself without a separate legend. +Small slices are filtered by `minPercentage` so long-tail labels do not collide +with the primary categories. + +```tsx + + `${label.split(" ")[0]} ${percentageLabel}` + }} + width={360} + height={260} +/> +``` From bb712ed29ec6d7facd4078acf22a4e34a1dc5c13 Mon Sep 17 00:00:00 2001 From: iexitdev Date: Fri, 29 May 2026 00:04:25 -0400 Subject: [PATCH 2/7] Add editable docs playground --- apps/site/package.json | 1 + .../src/lib/remark-strip-duplicate-title.mjs | 74 ++- apps/site/src/previews/ChartPlayground.tsx | 429 ++++++++++++++++++ apps/site/src/previews/client.tsx | 28 ++ apps/site/src/styles/starlight.css | 387 +++++++++++++++- package-lock.json | 126 ++++- 6 files changed, 1030 insertions(+), 15 deletions(-) create mode 100644 apps/site/src/previews/ChartPlayground.tsx diff --git a/apps/site/package.json b/apps/site/package.json index dc13ef3c..bb6ed3f9 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -22,6 +22,7 @@ "dependencies": { "@tailwindcss/vite": "^4.3.0", "lucide-astro": "^0.556.0", + "react-live": "^4.1.8", "tailwindcss": "^4.3.0" } } diff --git a/apps/site/src/lib/remark-strip-duplicate-title.mjs b/apps/site/src/lib/remark-strip-duplicate-title.mjs index 8e620aa1..0818cc7b 100644 --- a/apps/site/src/lib/remark-strip-duplicate-title.mjs +++ b/apps/site/src/lib/remark-strip-duplicate-title.mjs @@ -98,7 +98,71 @@ const escapeAttribute = (value) => .replace(/"/g, """) .replace(/ encodeURIComponent(String(value)); + +const getPreviewHtml = (id, title) => { + const titleAttribute = + typeof title === "string" && title.length > 0 + ? ` data-preview-title="${escapeAttribute(title)}"` + : ""; + + return `
Loading chart preview
`; +}; + const transformPreviewDirectives = (tree) => { + if (Array.isArray(tree.children)) { + for (let index = 0; index < tree.children.length; index += 1) { + const node = tree.children[index]; + + if (node.type !== "leafDirective" || node.name !== "chart-preview") { + continue; + } + + const id = node.attributes?.id; + + if (typeof id !== "string" || id.length === 0) { + node.type = "html"; + node.value = + '
Missing chart preview id
'; + node.children = []; + continue; + } + + const previousNode = tree.children[index - 1]; + + if ( + editablePreviewIds.has(id) && + previousNode?.type === "code" && + ["jsx", "tsx"].includes(previousNode.lang) + ) { + const title = node.attributes?.title; + const titleAttribute = + typeof title === "string" && title.length > 0 + ? ` data-preview-title="${escapeAttribute(title)}"` + : ""; + + previousNode.type = "html"; + previousNode.value = `
Loading chart playground
`; + previousNode.children = []; + tree.children.splice(index, 1); + index -= 1; + continue; + } + + node.type = "html"; + node.value = getPreviewHtml(id, node.attributes?.title); + node.children = []; + } + } + visit(tree, (node) => { if (node.type !== "leafDirective" || node.name !== "chart-preview") { return; @@ -114,16 +178,8 @@ const transformPreviewDirectives = (tree) => { return; } - const title = node.attributes?.title; - const titleAttribute = - typeof title === "string" && title.length > 0 - ? ` data-preview-title="${escapeAttribute(title)}"` - : ""; - node.type = "html"; - node.value = `
Loading chart preview
`; + node.value = getPreviewHtml(id, node.attributes?.title); node.children = []; }); }; diff --git a/apps/site/src/previews/ChartPlayground.tsx b/apps/site/src/previews/ChartPlayground.tsx new file mode 100644 index 00000000..9cd935bd --- /dev/null +++ b/apps/site/src/previews/ChartPlayground.tsx @@ -0,0 +1,429 @@ +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from "react"; +import { Text, View } from "react-native"; +import { + LiveContext, + LiveEditor, + LiveError, + LivePreview, + LiveProvider +} from "react-live"; + +import { + AreaChart, + BarChart, + ChartKitProvider, + ContributionGraph, + DonutChart, + LineChart, + PieChart, + ProgressChart, + type ChartKitThemeMode +} from "react-native-chart-kit/v2"; + +import { + acquisitionShare, + clampChartWidth, + contributionValues, + money, + monthRevenue, + percent, + platformShare, + profit, + progressRings, + revenueMix, + signedMoney, + signups, + supportVolume +} from "./data"; +import { chartPreviewExamples } from "./registry"; +import { showcaseCustomPresets } from "./showcaseTheme"; + +const importStatementPattern = + /^\s*import(?:[\s\S]*?\sfrom\s+["'][^"']+["']|(?:\s+type)?\s+["'][^"']+["']);?\s*/gm; + +const getThemeMode = (): Exclude => + document.documentElement.dataset.theme === "light" ? "light" : "dark"; + +const getComponentName = (code: string) => { + const declaration = + code.match(/\bexport\s+default\s+function\s+([A-Z][\w]*)/) ?? + code.match(/\bexport\s+function\s+([A-Z][\w]*)/) ?? + code.match(/\bfunction\s+([A-Z][\w]*)/) ?? + code.match(/\bexport\s+const\s+([A-Z][\w]*)\s*=/) ?? + code.match(/\bconst\s+([A-Z][\w]*)\s*=/); + + return declaration?.[1]; +}; + +const prepareLiveCode = (code: string) => { + const componentName = getComponentName(code); + const runnableCode = code + .replace(importStatementPattern, "") + .replace(/\bexport\s+default\s+function\s+/g, "function ") + .replace(/\bexport\s+function\s+/g, "function ") + .replace(/\bexport\s+const\s+/g, "const ") + .replace(/\bexport\s+default\s+/g, "") + .trim(); + + if (/render\s*\(/.test(runnableCode)) { + return `(() => {\n${runnableCode}\n})();`; + } + + if (componentName) { + return `(() => {\n${runnableCode}\n\nrender(<${componentName} />);\n})();`; + } + + return `(() => {\nrender(${runnableCode});\n})();`; +}; + +const decodeInitialCode = (value: string) => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + +const DEFAULT_EDITOR_SIZE = 52; +const MIN_EDITOR_SIZE = 30; +const MAX_EDITOR_SIZE = 70; + +const clamp = (value: number, min: number, max: number) => + Math.min(max, Math.max(min, value)); + +const writeClipboard = async (value: string) => { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.setAttribute("readonly", "true"); + textarea.style.position = "fixed"; + textarea.style.top = "0"; + textarea.style.left = "0"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); +}; + +const TypeScriptLogo = () => ( + +); + +const CopyIcon = () => ( + +); + +const CodePaneHeader = ({ codeToCopy }: { codeToCopy: string }) => { + const [copied, setCopied] = useState(false); + const copiedResetRef = useRef(undefined); + + useEffect( + () => () => { + if (copiedResetRef.current) { + window.clearTimeout(copiedResetRef.current); + } + }, + [] + ); + + const copyCode = useCallback(async () => { + await writeClipboard(codeToCopy).catch(() => undefined); + setCopied(true); + + if (copiedResetRef.current) { + window.clearTimeout(copiedResetRef.current); + } + + copiedResetRef.current = window.setTimeout(() => { + setCopied(false); + }, 1800); + }, [codeToCopy]); + + return ( +
+ + + TypeScript + + +
+ ); +}; + +const LiveCodeEditor = ({ + onCodeChange +}: { + onCodeChange: (value: string) => void; +}) => { + const { onChange } = useContext(LiveContext); + + return ( + { + onCodeChange(value); + onChange(value); + }} + tabMode="indentation" + /> + ); +}; + +export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { + const frameRef = useRef(null); + const gridRef = useRef(null); + const previewPaneRef = useRef(null); + const [mode, setMode] = useState>(() => + getThemeMode() + ); + const [editorSize, setEditorSize] = useState(DEFAULT_EDITOR_SIZE); + const [isResizing, setIsResizing] = useState(false); + const [width, setWidth] = useState(420); + const initialCode = useMemo(() => decodeInitialCode(code), [code]); + const [currentCode, setCurrentCode] = useState(() => initialCode); + const example = chartPreviewExamples[id]; + + useEffect(() => { + const previewPane = previewPaneRef.current; + + if (!previewPane) { + return; + } + + const resize = () => { + setWidth(Math.max(280, Math.floor(previewPane.clientWidth - 32))); + }; + const resizeObserver = new ResizeObserver(resize); + resizeObserver.observe(previewPane); + resize(); + + return () => resizeObserver.disconnect(); + }, []); + + useEffect(() => { + const updateMode = () => setMode(getThemeMode()); + const themeObserver = new MutationObserver(updateMode); + themeObserver.observe(document.documentElement, { + attributeFilter: ["data-theme"] + }); + updateMode(); + + return () => themeObserver.disconnect(); + }, []); + + const scope = useMemo( + () => ({ + AreaChart, + BarChart, + ContributionGraph, + DonutChart, + LineChart, + PieChart, + ProgressChart, + React, + Text, + View, + acquisitionShare, + clampChartWidth, + contributionValues, + data: monthRevenue, + money, + monthRevenue, + percent, + platformShare, + previewWidth: clampChartWidth(width), + profit, + progressRings, + revenueMix, + signedMoney, + signups, + supportVolume + }), + [width] + ); + + const playgroundStyle = useMemo( + () => + ({ + "--chart-kit-editor-size": `${editorSize}%` + }) as React.CSSProperties, + [editorSize] + ); + + const resizeFromClientX = useCallback((clientX: number) => { + const grid = gridRef.current; + + if (!grid) { + return; + } + + const rect = grid.getBoundingClientRect(); + const nextSize = ((clientX - rect.left) / rect.width) * 100; + setEditorSize(clamp(nextSize, MIN_EDITOR_SIZE, MAX_EDITOR_SIZE)); + }, []); + + const adjustEditorSize = useCallback((delta: number) => { + setEditorSize((size) => + clamp(size + delta, MIN_EDITOR_SIZE, MAX_EDITOR_SIZE) + ); + }, []); + + return ( +
+ +
+
+ + +
+ +
+
+
+ ); +}; diff --git a/apps/site/src/previews/client.tsx b/apps/site/src/previews/client.tsx index 3516d89f..cf8b3477 100644 --- a/apps/site/src/previews/client.tsx +++ b/apps/site/src/previews/client.tsx @@ -1,6 +1,7 @@ import React from "react"; import { createRoot, type Root } from "react-dom/client"; +import { ChartPlayground } from "./ChartPlayground"; import { ChartPreview } from "./ChartPreview"; const roots = new WeakMap(); @@ -17,22 +18,49 @@ const mountPreview = (element: Element) => { root.render(); }; +const mountPlayground = (element: Element) => { + const id = element.getAttribute("data-preview-id"); + const code = element.getAttribute("data-code"); + + if (!id || !code || roots.has(element)) { + return; + } + + const root = createRoot(element); + roots.set(element, root); + root.render(); +}; + class ChartKitPreviewElement extends HTMLElement { connectedCallback() { mountPreview(this); } } +class ChartKitPlaygroundElement extends HTMLElement { + connectedCallback() { + mountPlayground(this); + } +} + const scan = () => { const previews = Array.from(document.querySelectorAll("chart-kit-preview")); + const playgrounds = Array.from( + document.querySelectorAll("chart-kit-playground") + ); previews.forEach(mountPreview); + playgrounds.forEach(mountPlayground); }; if (!customElements.get("chart-kit-preview")) { customElements.define("chart-kit-preview", ChartKitPreviewElement); } +if (!customElements.get("chart-kit-playground")) { + customElements.define("chart-kit-playground", ChartKitPlaygroundElement); +} + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", scan, { once: true }); } else { diff --git a/apps/site/src/styles/starlight.css b/apps/site/src/styles/starlight.css index 025f299c..c57ab088 100644 --- a/apps/site/src/styles/starlight.css +++ b/apps/site/src/styles/starlight.css @@ -10,9 +10,9 @@ :root { --sl-font: "Inter", system-ui, sans-serif; - --sl-content-width: 47rem; - --sl-sidebar-width: 18rem; - --sl-content-pad-x: 1.25rem; + --sl-content-width: 58rem; + --sl-sidebar-width: 17rem; + --sl-content-pad-x: 2.5rem; --sl-content-gap-y: 1.15rem; --sl-line-height: 1.72; --sl-line-height-headings: 1.08; @@ -216,6 +216,51 @@ button[data-open-modal] { padding-top: 2.65rem; } +@media (min-width: 72rem) { + :root { + --sl-content-width: 62rem; + --sl-content-pad-x: 3.75rem; + --sl-sidebar-pad-x: 0.75rem; + --sl-sidebar-width: 16.25rem; + } + + .right-sidebar-container { + width: 14.5rem; + } + + .right-sidebar { + width: 14.5rem; + } + + [data-has-sidebar][data-has-toc] .main-pane { + --sl-content-margin-inline: auto; + + width: calc(100% - 14.5rem); + } + + .right-sidebar-panel { + padding-inline: 0.75rem; + } + + .right-sidebar-panel .sl-container { + width: auto; + max-width: 13rem; + } +} + +@media (max-width: 71.99rem) { + :root { + --sl-content-pad-x: 3rem; + } +} + +@media (min-width: 92rem) { + :root { + --sl-content-width: 66rem; + --sl-content-pad-x: 4.5rem; + } +} + .sl-markdown-content { color: var(--sl-color-text); } @@ -274,6 +319,317 @@ button[data-open-modal] { border-radius: 0.8rem; } +chart-kit-playground { + display: block; + margin-block: 1.25rem 2rem; +} + +.chart-kit-playground__frame { + overflow: hidden; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.9rem; + background: #10131a; +} + +.chart-kit-playground__frame[data-resizing="true"] { + cursor: col-resize; + user-select: none; +} + +.chart-kit-playground__frame[data-resizing="true"] * { + cursor: col-resize !important; +} + +:root[data-theme="light"] .chart-kit-playground__frame { + background: #ffffff; +} + +.chart-kit-playground__grid { + display: grid; + grid-template-columns: + minmax(18rem, var(--chart-kit-editor-size, 52%)) 1px + minmax(18rem, 1fr); + min-height: 28rem; +} + +.chart-kit-playground__frame .chart-kit-playground__editor-pane, +.chart-kit-playground__frame .chart-kit-playground__preview-pane { + display: flex; + flex-direction: column; + margin: 0; + min-width: 0; +} + +.chart-kit-playground__editor-pane { + background: #11141c; +} + +:root[data-theme="light"] .chart-kit-playground__editor-pane { + background: #f8fbff; +} + +.chart-kit-playground__preview-pane { + min-height: 28rem; + background: #0f1218; +} + +:root[data-theme="light"] .chart-kit-playground__preview-pane { + background: #ffffff; +} + +.chart-kit-playground__resize-handle { + position: relative; + z-index: 2; + display: block; + width: 1px; + min-width: 1px; + align-self: stretch; + border: 0; + background: transparent; + cursor: col-resize; + padding: 0; +} + +.chart-kit-playground__resize-handle::before { + position: absolute; + z-index: 1; + top: 0; + bottom: 0; + left: -0.45rem; + width: 0.9rem; + content: ""; +} + +.chart-kit-playground__resize-handle::after { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + background: var(--sl-color-hairline); + content: ""; + transform: translateX(-50%); +} + +.chart-kit-playground__resize-handle:hover::after, +.chart-kit-playground__resize-handle:focus-visible::after, +.chart-kit-playground__frame[data-resizing="true"] + .chart-kit-playground__resize-handle::after { + width: 2px; + background: #7aa7ff; +} + +.chart-kit-playground__resize-handle:focus-visible { + outline: none; +} + +.chart-kit-playground__pane-header { + box-sizing: border-box; + display: flex; + flex: 0 0 2.75rem; + height: 2.75rem; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid var(--sl-color-hairline); + color: var(--sl-color-gray-2); + font-size: 0.78rem; + font-weight: 760; + letter-spacing: 0; + line-height: 1; + padding-inline: 0.95rem; +} + +.chart-kit-playground__editor-pane .chart-kit-playground__pane-header { + padding-right: 0.55rem; +} + +.chart-kit-playground__language-label { + display: inline-flex; + align-items: center; + gap: 0.45rem; + min-width: 0; +} + +.chart-kit-playground__ts-logo { + display: block; + width: 1.05rem; + height: 1.05rem; + flex: 0 0 auto; +} + +.chart-kit-playground__copy-button { + position: relative; + display: inline-flex; + width: 1.55rem; + height: 1.55rem; + align-items: center; + justify-content: center; + flex: 0 0 auto; + border: 1px solid transparent; + border-radius: 0.45rem; + background: transparent; + color: var(--sl-color-gray-3); + cursor: pointer; + padding: 0; +} + +.chart-kit-playground__copy-button:hover, +.chart-kit-playground__copy-button:focus-visible, +.chart-kit-playground__copy-button[data-copied="true"] { + border-color: transparent; + background: transparent; + color: var(--sl-color-white); +} + +:root[data-theme="light"] .chart-kit-playground__copy-button:hover, +:root[data-theme="light"] .chart-kit-playground__copy-button:focus-visible, +:root[data-theme="light"] + .chart-kit-playground__copy-button[data-copied="true"] { + background: transparent; + color: #071733; +} + +.chart-kit-playground__copy-button:focus-visible { + outline: 2px solid rgba(120, 168, 255, 0.55); + outline-offset: 2px; +} + +.chart-kit-playground__copy-icon { + width: 1rem; + height: 1rem; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.9; +} + +.chart-kit-playground__copy-tooltip { + position: absolute; + z-index: 4; + right: 0; + top: calc(100% + 0.5rem); + width: max-content; + max-width: 11rem; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.45rem; + background: #161a23; + color: var(--sl-color-white); + font-size: 0.72rem; + font-weight: 650; + line-height: 1; + opacity: 0; + padding: 0.45rem 0.55rem; + pointer-events: none; + transform: translateY(-0.2rem); + transition: + opacity 0.12s ease, + transform 0.12s ease; + visibility: hidden; +} + +.chart-kit-playground__copy-tooltip::after { + position: absolute; + right: 0.65rem; + top: -0.3rem; + width: 0.5rem; + height: 0.5rem; + border-top: 1px solid var(--sl-color-hairline); + border-left: 1px solid var(--sl-color-hairline); + background: inherit; + content: ""; + transform: rotate(45deg); +} + +:root[data-theme="light"] .chart-kit-playground__copy-tooltip { + background: #ffffff; + color: #071733; +} + +.chart-kit-playground__copy-button:hover .chart-kit-playground__copy-tooltip, +.chart-kit-playground__copy-button:focus-visible + .chart-kit-playground__copy-tooltip, +.chart-kit-playground__copy-button[data-copied="true"] + .chart-kit-playground__copy-tooltip { + opacity: 1; + transform: translateY(0); + visibility: visible; +} + +.chart-kit-playground__preview-title { + overflow: hidden; + color: var(--sl-color-gray-3); + font-weight: 680; + letter-spacing: 0; + text-overflow: ellipsis; + text-transform: none; + white-space: nowrap; +} + +.chart-kit-playground__editor { + min-height: 0; + flex: 1 1 auto; + color: #e8edf8; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.79rem; + line-height: 1.65; + overflow: hidden; +} + +:root[data-theme="light"] .chart-kit-playground__editor { + color: #071733; +} + +.chart-kit-playground__editor pre { + border: 0 !important; + border-radius: 0 !important; + min-height: 25.45rem; + max-height: 34rem; + overflow: auto; + background: transparent !important; + caret-color: currentColor; + color: inherit; + outline: none; + padding: 1rem !important; + tab-size: 2; + white-space: pre; +} + +.chart-kit-playground__editor pre:focus-visible { + outline: 2px solid rgba(120, 168, 255, 0.55); + outline-offset: -2px; +} + +:root[data-theme="light"] .chart-kit-playground__editor pre:focus-visible { + outline-color: rgba(37, 99, 235, 0.45); +} + +.chart-kit-playground__preview-surface { + display: flex; + min-height: 22rem; + flex: 1; + align-items: center; + justify-content: center; + overflow: auto; + padding: 1.25rem; +} + +.chart-kit-playground__error { + margin: 0; + border-top: 1px solid rgba(220, 38, 38, 0.35); + background: rgba(220, 38, 38, 0.1); + color: #fecaca; + font-size: 0.78rem; + line-height: 1.5; + padding: 0.85rem 1rem; + white-space: pre-wrap; +} + +:root[data-theme="light"] .chart-kit-playground__error { + color: #991b1b; +} + .right-sidebar { border-color: var(--sl-color-hairline); } @@ -315,6 +671,10 @@ button[data-open-modal] { } @media (max-width: 49.99rem) { + :root { + --sl-content-pad-x: 1.25rem; + } + .site-title { font-size: 0.86rem; } @@ -331,4 +691,25 @@ button[data-open-modal] { .sl-markdown-content h2:not(:where(.not-content *)) { font-size: 1.6rem; } + + .chart-kit-playground__grid { + grid-template-columns: 1fr; + } + + .chart-kit-playground__editor-pane { + border-bottom: 1px solid var(--sl-color-hairline); + } + + .chart-kit-playground__resize-handle { + display: none; + } + + .chart-kit-playground__editor pre { + min-height: 20rem; + max-height: 24rem; + } + + .chart-kit-playground__preview-pane { + min-height: 23rem; + } } diff --git a/package-lock.json b/package-lock.json index 9f480188..78c4de68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "dependencies": { "@tailwindcss/vite": "^4.3.0", "lucide-astro": "^0.556.0", + "react-live": "^4.1.8", "tailwindcss": "^4.3.0" }, "devDependencies": { @@ -3440,6 +3441,12 @@ "undici-types": "~7.19.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "devOptional": true, @@ -4153,6 +4160,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "license": "ISC", @@ -8990,7 +9003,6 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "devOptional": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -10868,6 +10880,17 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "funding": [ @@ -11030,7 +11053,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11604,6 +11626,19 @@ "version": "18.3.1", "license": "MIT" }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/prismjs": { "version": "1.30.0", "license": "MIT", @@ -11758,7 +11793,6 @@ }, "node_modules/react-dom": { "version": "19.2.6", - "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -11772,6 +11806,25 @@ "dev": true, "license": "MIT" }, + "node_modules/react-live": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/react-live/-/react-live-4.1.8.tgz", + "integrity": "sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==", + "license": "MIT", + "dependencies": { + "prism-react-renderer": "^2.4.0", + "sucrase": "^3.35.0", + "use-editable": "^2.3.3" + }, + "engines": { + "node": ">= 0.12.0", + "npm": ">= 2.0.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/react-native": { "version": "0.83.9", "license": "MIT", @@ -13311,6 +13364,37 @@ "dev": true, "license": "MIT" }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/suf-log": { "version": "2.5.3", "dev": true, @@ -13432,6 +13516,27 @@ "node": ">=8" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throat": { "version": "5.0.0", "license": "MIT" @@ -13551,6 +13656,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, "node_modules/type-detect": { "version": "4.0.8", "license": "MIT", @@ -14234,6 +14345,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-editable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/use-editable/-/use-editable-2.3.3.tgz", + "integrity": "sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "devOptional": true, From f7d49a4a13538c5aa26192a24e15cef47a2fcf51 Mon Sep 17 00:00:00 2001 From: iexitdev Date: Fri, 29 May 2026 00:08:58 -0400 Subject: [PATCH 3/7] Enable editable playgrounds for chart examples --- .../src/lib/remark-strip-duplicate-title.mjs | 96 +++++++++-- apps/site/src/previews/ChartPlayground.tsx | 158 +++++++++++++++++- docs/charts/progress.md | 14 +- 3 files changed, 246 insertions(+), 22 deletions(-) diff --git a/apps/site/src/lib/remark-strip-duplicate-title.mjs b/apps/site/src/lib/remark-strip-duplicate-title.mjs index 0818cc7b..200e6762 100644 --- a/apps/site/src/lib/remark-strip-duplicate-title.mjs +++ b/apps/site/src/lib/remark-strip-duplicate-title.mjs @@ -98,10 +98,30 @@ const escapeAttribute = (value) => .replace(/"/g, """) .replace(/ encodeURIComponent(String(value)); +const playgroundDocs = new Set([ + "getting-started/installation.md", + "recipes/README.md", + "charts/area.md", + "charts/bar.md", + "charts/contribution-heatmap.md", + "charts/donut.md", + "charts/line.md", + "charts/pie.md", + "charts/progress.md" +]); + +const chartComponentPattern = + /<\s*(AreaChart|BarChart|ContributionGraph|DonutChart|LineChart|PieChart|ProgressChart|ProgressRing|StackedBarChart)\b/; + +const slugify = (value) => + String(value) + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + const getPreviewHtml = (id, title) => { const titleAttribute = typeof title === "string" && title.length > 0 @@ -113,11 +133,68 @@ const getPreviewHtml = (id, title) => { )}"${titleAttribute}>
Loading chart preview
`; }; -const transformPreviewDirectives = (tree) => { +const getPlaygroundHtml = (id, code, title) => { + const titleAttribute = + typeof title === "string" && title.length > 0 + ? ` data-preview-title="${escapeAttribute(title)}"` + : ""; + + return `
Loading chart playground
`; +}; + +const isRenderableChartExample = (node, docsPath) => + playgroundDocs.has(docsPath) && + node?.type === "code" && + ["jsx", "tsx"].includes(node.lang) && + chartComponentPattern.test(node.value ?? ""); + +const getGeneratedPreviewId = (docsPath, title, index) => { + const pathSlug = slugify( + docsPath.replace(/\/README\.md$/, "").replace(/\.mdx?$/, "") + ); + const titleSlug = slugify(title || "example"); + + return `${pathSlug}-${titleSlug || "example"}-${index}`; +}; + +const transformPreviewDirectives = (tree, file) => { + const docsPath = getDocsEntryPath(file); + if (Array.isArray(tree.children)) { + let currentHeading = file.data?.astro?.frontmatter?.title ?? ""; + let generatedPreviewIndex = 0; + for (let index = 0; index < tree.children.length; index += 1) { const node = tree.children[index]; + if (node.type === "heading" && node.depth >= 2) { + currentHeading = textFromNode(node).trim(); + continue; + } + + if ( + isRenderableChartExample(node, docsPath) && + tree.children[index + 1]?.type !== "leafDirective" + ) { + generatedPreviewIndex += 1; + node.type = "html"; + node.value = getPlaygroundHtml( + getGeneratedPreviewId( + docsPath, + currentHeading, + generatedPreviewIndex + ), + node.value, + currentHeading + ); + node.children = []; + continue; + } + if (node.type !== "leafDirective" || node.name !== "chart-preview") { continue; } @@ -135,22 +212,13 @@ const transformPreviewDirectives = (tree) => { const previousNode = tree.children[index - 1]; if ( - editablePreviewIds.has(id) && previousNode?.type === "code" && ["jsx", "tsx"].includes(previousNode.lang) ) { const title = node.attributes?.title; - const titleAttribute = - typeof title === "string" && title.length > 0 - ? ` data-preview-title="${escapeAttribute(title)}"` - : ""; previousNode.type = "html"; - previousNode.value = `
Loading chart playground
`; + previousNode.value = getPlaygroundHtml(id, previousNode.value, title); previousNode.children = []; tree.children.splice(index, 1); index -= 1; @@ -223,6 +291,6 @@ export default function stripDuplicateTitle() { }); rewriteMarkdownLinks(tree, file); - transformPreviewDirectives(tree); + transformPreviewDirectives(tree, file); }; } diff --git a/apps/site/src/previews/ChartPlayground.tsx b/apps/site/src/previews/ChartPlayground.tsx index 9cd935bd..d2c8f313 100644 --- a/apps/site/src/previews/ChartPlayground.tsx +++ b/apps/site/src/previews/ChartPlayground.tsx @@ -20,12 +20,16 @@ import { BarChart, ChartKitProvider, ContributionGraph, + createChartPreset, DonutChart, LineChart, PieChart, ProgressChart, + ProgressRing, + StackedBarChart, type ChartKitThemeMode } from "react-native-chart-kit/v2"; +import { G, Line as SvgLine, Rect, Text as SvgText } from "react-native-svg"; import { acquisitionShare, @@ -62,6 +66,34 @@ const getComponentName = (code: string) => { return declaration?.[1]; }; +const getStatementStyleLiveCode = (code: string) => { + const lines = code.split("\n"); + const firstJsxLineIndex = lines.findIndex((line) => + /^\s*<[A-Z][\w.]*/.test(line) + ); + + if (firstJsxLineIndex === -1) { + return undefined; + } + + const setupCode = lines.slice(0, firstJsxLineIndex).join("\n").trim(); + const jsxCode = lines + .slice(firstJsxLineIndex) + .join("\n") + .trim() + .replace(/(\/>|<\/[A-Z][\w.]*>);/g, "$1"); + + return `function ChartKitLiveExample() { +${setupCode ? `${setupCode}\n\n` : ""}return ( + <> +${jsxCode} + +); +} + +render();`; +}; + const prepareLiveCode = (code: string) => { const componentName = getComponentName(code); const runnableCode = code @@ -80,6 +112,12 @@ const prepareLiveCode = (code: string) => { return `(() => {\n${runnableCode}\n\nrender(<${componentName} />);\n})();`; } + const statementStyleLiveCode = getStatementStyleLiveCode(runnableCode); + + if (statementStyleLiveCode) { + return `(() => {\n${statementStyleLiveCode}\n})();`; + } + return `(() => {\nrender(${runnableCode});\n})();`; }; @@ -98,6 +136,93 @@ const MAX_EDITOR_SIZE = 70; const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); +const barPlaygroundData = signups.map((row) => ({ + ...row, + newCustomers: row.signups, + organic: row.signups, + paid: row.expansion, + spend: row.signups, + week: row.month +})); + +const linePlaygroundData = monthRevenue.map((row, index) => { + const barRow = barPlaygroundData[index % barPlaygroundData.length]; + + return { + ...row, + actual: row.revenue, + attainment: row.retention, + benchmark: row.forecast, + date: row.month, + expansion: barRow.expansion, + newCustomers: barRow.newCustomers, + organic: barRow.organic, + paid: barRow.paid, + portfolio: row.revenue, + price: row.revenue, + signups: barRow.signups, + spend: barRow.spend, + timestamp: index + 1, + week: barRow.week + }; +}); + +const revenueMixPlaygroundData = revenueMix.map((row) => ({ + ...row, + plan: row.label, + revenue: row.value +})); + +const acquisitionSharePlaygroundData = acquisitionShare.map((row) => ({ + ...row, + share: row.value +})); + +const weeklySpend = Array.from({ length: 14 }, (_, index) => ({ + spend: 24 + Math.round(Math.sin(index / 2) * 8 + index * 2.4), + week: `W${index + 1}` +})); + +const weeklyAcquisition = weeklySpend.map((row, index) => ({ + ...row, + organic: 32 + index * 4, + paid: 18 + Math.round(Math.cos(index / 2) * 6 + index * 2) +})); + +const portfolioHistory = Array.from({ length: 120 }, (_, index) => { + const portfolio = 84000 + index * 620 + Math.sin(index / 5) * 4200; + const benchmark = 81000 + index * 520 + Math.cos(index / 7) * 3200; + + return { + benchmark, + date: `Day ${index + 1}`, + month: `Day ${index + 1}`, + portfolio, + price: portfolio, + timestamp: index + 1 + }; +}); + +const largeData = portfolioHistory.map((row, index) => ({ + ...row, + price: 120 + index * 1.8 + Math.sin(index / 4) * 16 +})); + +const retentionSegments = [ + { accounts: 124, color: "#00163f", status: "Active" }, + { accounts: 46, color: "#2f5f9f", status: "At risk" }, + { accounts: 18, color: "#6f88aa", status: "Paused" } +]; + +const previewDataById: Record = { + "bar-grouped": barPlaygroundData, + "line-multi-series": linePlaygroundData, + "line-selection": linePlaygroundData +}; + +const getPreviewData = (id: string) => + previewDataById[id] ?? linePlaygroundData; + const writeClipboard = async (value: string) => { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); @@ -262,30 +387,53 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { AreaChart, BarChart, ContributionGraph, + ChartKitProvider, DonutChart, + G, LineChart, PieChart, ProgressChart, + ProgressRing, React, + Rect, + StackedBarChart, + SvgText, Text, View, - acquisitionShare, + acquisitionShare: acquisitionSharePlaygroundData, clampChartWidth, contributionValues, - data: monthRevenue, + createChartPreset, + data: getPreviewData(id), + largeData, money, monthRevenue, percent, + plans: revenueMixPlaygroundData, platformShare, + portfolioHistory, previewWidth: clampChartWidth(width), profit, progressRings, - revenueMix, + revenueMix: revenueMixPlaygroundData, + retentionSegments, signedMoney, signups, - supportVolume + setHeaderValue: () => undefined, + setSelectedChannel: () => undefined, + setSelectedDay: () => undefined, + setViewport: () => undefined, + supportVolume, + SvgLine, + Line: SvgLine, + usageDays: contributionValues, + useState: React.useState, + values: contributionValues, + viewport: { endIndex: 90, startIndex: 40 }, + weeklyAcquisition, + weeklySpend }), - [width] + [id, width] ); const playgroundStyle = useMemo( diff --git a/docs/charts/progress.md b/docs/charts/progress.md index 2b20798d..1ff93bce 100644 --- a/docs/charts/progress.md +++ b/docs/charts/progress.md @@ -7,8 +7,10 @@ description: Show circular progress values with configurable labels and accessib The v2 progress surface supports concentric rings and single-ring completion states. It accepts object rows for the modern API and the legacy Chart Kit progress data shape. +## Concentric Rings + ```tsx -import { ProgressChart, ProgressRing } from "react-native-chart-kit/v2"; +import { ProgressChart } from "react-native-chart-kit/v2"; const data = [ { metric: "Move", progress: 0.72, color: "#f43f5e" }, @@ -26,6 +28,14 @@ const data = [ preset="health" centerLabel={({ average }) => `${Math.round(average * 100)}%`} />; +``` + +::chart-preview{id="progress-rings"} + +## Single Ring + +```tsx +import { ProgressRing } from "react-native-chart-kit/v2"; ; ``` -::chart-preview{id="progress-rings"} - ## Compatibility Shape The legacy data object still works: From 322a72f4a5edb945ae007e183b8a808dd4ec0f2e Mon Sep 17 00:00:00 2001 From: iexitdev Date: Fri, 29 May 2026 23:06:22 -0400 Subject: [PATCH 4/7] docs: refresh chart docs and previews --- apps/site/astro.config.mjs | 5 +- apps/site/src/chart-theme-controls.ts | 218 +++++++++ apps/site/src/components/Head.astro | 30 +- apps/site/src/components/ThemeInit.astro | 24 + .../src/lib/remark-strip-duplicate-title.mjs | 10 +- apps/site/src/previews/ChartPlayground.tsx | 131 +++++- apps/site/src/previews/ChartPreview.tsx | 29 +- apps/site/src/previews/chartTheme.ts | 82 ++++ apps/site/src/previews/data.ts | 113 +++-- apps/site/src/previews/examples.tsx | 11 +- apps/site/src/previews/reactNativeWebStub.tsx | 197 ++++++++- apps/site/src/previews/registry.tsx | 16 +- apps/site/src/styles/starlight.css | 418 +++++++++++++++++- docs/README.md | 12 +- docs/charts/area.md | 100 ++++- docs/charts/bar.md | 184 +++++++- docs/charts/contribution-heatmap.md | 63 ++- docs/charts/donut.md | 58 ++- docs/charts/line.md | 163 ++++++- docs/charts/pie.md | 50 ++- docs/charts/progress.md | 73 ++- docs/charts/themes.md | 22 + docs/getting-started/contributing.md | 83 ++++ docs/getting-started/installation.md | 51 +-- docs/migration/from-v1.md | 9 +- docs/recipes/README.md | 4 +- packages/react-native/README.md | 6 +- .../charts/contribution/ContributionGraph.tsx | 1 + .../react-native/src/charts/pie/PieChart.tsx | 1 + .../src/charts/progress/ProgressChart.tsx | 1 + scripts/typecheck-doc-examples.mjs | 1 + 31 files changed, 1976 insertions(+), 190 deletions(-) create mode 100644 apps/site/src/chart-theme-controls.ts create mode 100644 apps/site/src/previews/chartTheme.ts create mode 100644 docs/getting-started/contributing.md diff --git a/apps/site/astro.config.mjs b/apps/site/astro.config.mjs index a7f8baa4..2b518ba7 100644 --- a/apps/site/astro.config.mjs +++ b/apps/site/astro.config.mjs @@ -77,7 +77,10 @@ export default defineConfig({ sidebar: [ { label: "Start", - items: [{ slug: "docs/getting-started/installation" }] + items: [ + { slug: "docs/getting-started/installation" }, + { slug: "docs/getting-started/contributing" } + ] }, { label: "Charts", diff --git a/apps/site/src/chart-theme-controls.ts b/apps/site/src/chart-theme-controls.ts new file mode 100644 index 00000000..d24ed01d --- /dev/null +++ b/apps/site/src/chart-theme-controls.ts @@ -0,0 +1,218 @@ +import { + applyChartThemePreset, + chartThemeOptions, + getCurrentChartThemePreset, + isChartThemePreset +} from "./previews/chartTheme"; + +type ColorTheme = "dark" | "light"; + +const controlSelector = "[data-chart-theme-control]"; +const docsThemeToggleSelector = "[data-docs-theme-toggle]"; +const selectSelector = "[data-chart-theme-select]"; +const starlightThemeStorageKey = "starlight-theme"; +const themeColor = { + dark: "#06070a", + light: "#f7f8fb" +}; + +const isColorTheme = (value: string | null): value is ColorTheme => + value === "dark" || value === "light"; + +const getCurrentColorTheme = (): ColorTheme => { + try { + const stored = localStorage.getItem(starlightThemeStorageKey); + + if (isColorTheme(stored)) { + return stored; + } + } catch { + // Storage can be blocked in restricted browser contexts. + } + + return document.documentElement.dataset.theme === "light" ? "light" : "dark"; +}; + +const syncStarlightThemeSelects = (theme: ColorTheme) => { + document + .querySelectorAll("starlight-theme-select select") + .forEach((select) => { + if (select.value !== theme) { + select.value = theme; + } + }); +}; + +const syncDocsThemeToggles = (theme: ColorTheme) => { + const nextTheme = theme === "dark" ? "light" : "dark"; + + document + .querySelectorAll(docsThemeToggleSelector) + .forEach((button) => { + button.dataset.themeCurrent = theme; + button.dataset.themeNext = nextTheme; + button.setAttribute("aria-label", `Switch to ${nextTheme} mode`); + button.setAttribute("title", `Switch to ${nextTheme} mode`); + }); +}; + +const updateThemeColor = (theme: ColorTheme) => { + const meta = document.querySelector( + 'meta[name="theme-color"]' + ); + + if (meta) { + meta.content = themeColor[theme]; + } +}; + +const applyColorTheme = (theme: ColorTheme) => { + document.documentElement.dataset.theme = theme; + document.documentElement.style.colorScheme = theme; + + try { + localStorage.setItem(starlightThemeStorageKey, theme); + } catch { + // Persisting the preference is best-effort. + } + + syncStarlightThemeSelects(theme); + syncDocsThemeToggles(theme); + updateThemeColor(theme); + window.StarlightThemeProvider?.updatePickers(theme); +}; + +const syncChartThemeControls = () => { + const theme = getCurrentChartThemePreset(); + + document + .querySelectorAll(selectSelector) + .forEach((select) => { + if (select.value !== theme) { + select.value = theme; + } + }); +}; + +const createChartThemeControl = () => { + const label = document.createElement("label"); + label.className = "chartkit-chart-theme-control"; + label.dataset.chartThemeControl = "true"; + + const text = document.createElement("span"); + text.className = "chartkit-chart-theme-control__label"; + text.textContent = "Chart theme"; + + const select = document.createElement("select"); + select.className = "chartkit-chart-theme-control__select"; + select.dataset.chartThemeSelect = "true"; + select.setAttribute("aria-label", "Chart theme"); + + chartThemeOptions.forEach((option) => { + const item = document.createElement("option"); + item.value = option.value; + item.textContent = option.label; + select.append(item); + }); + + label.append(text, select); + + return label; +}; + +const createDocsThemeToggle = () => { + const button = document.createElement("button"); + button.className = "chartkit-docs-theme-toggle"; + button.dataset.docsThemeToggle = "true"; + button.type = "button"; + button.innerHTML = ` + + `; + + return button; +}; + +const enhanceThemeSelectors = () => { + document + .querySelectorAll("starlight-theme-select") + .forEach((themeSelect) => { + const previousElement = themeSelect.previousElementSibling; + + if (!previousElement?.matches(controlSelector)) { + themeSelect.before(createChartThemeControl()); + } + + if (themeSelect.dataset.chartkitIconToggle !== "true") { + themeSelect.dataset.chartkitIconToggle = "true"; + themeSelect.append(createDocsThemeToggle()); + } + }); + + syncChartThemeControls(); + syncDocsThemeToggles(getCurrentColorTheme()); +}; + +const handleChartThemeChange = (event: Event) => { + const select = (event.target as Element | null)?.closest( + selectSelector + ); + + if (!select || !isChartThemePreset(select.value)) { + return; + } + + applyChartThemePreset(select.value); + syncChartThemeControls(); +}; + +const handleDocsThemeClick = (event: Event) => { + const button = (event.target as Element | null)?.closest( + docsThemeToggleSelector + ); + + if (!button) { + return; + } + + const nextTheme = getCurrentColorTheme() === "dark" ? "light" : "dark"; + applyColorTheme(nextTheme); +}; + +const bootChartThemeControls = () => { + enhanceThemeSelectors(); + applyChartThemePreset(getCurrentChartThemePreset(), { dispatch: false }); + applyColorTheme(getCurrentColorTheme()); + + if (!window.__chartkitChartThemeControlBound) { + window.__chartkitChartThemeControlBound = true; + document.addEventListener("change", handleChartThemeChange); + document.addEventListener("click", handleDocsThemeClick); + } +}; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", bootChartThemeControls, { + once: true + }); +} else { + bootChartThemeControls(); +} + +customElements.whenDefined("starlight-theme-select").then(() => { + bootChartThemeControls(); +}); + +document.addEventListener("astro:page-load", bootChartThemeControls); + +declare global { + interface Window { + StarlightThemeProvider?: { + updatePickers: (theme?: string) => void; + }; + __chartkitChartThemeControlBound?: boolean; + } +} + +export {}; diff --git a/apps/site/src/components/Head.astro b/apps/site/src/components/Head.astro index 4a8d4cd7..6153d2df 100644 --- a/apps/site/src/components/Head.astro +++ b/apps/site/src/components/Head.astro @@ -60,12 +60,40 @@ const reactRefreshPreamble = ` color-scheme: dark; } -{head.map(({ tag: Tag, attrs, content }) => )} +{ + head.map(({ tag: Tag, attrs, content }) => ( + + )) +} { isDev ? ( + diff --git a/apps/site/src/components/ThemeInit.astro b/apps/site/src/components/ThemeInit.astro index cc64463f..1912a4ad 100644 --- a/apps/site/src/components/ThemeInit.astro +++ b/apps/site/src/components/ThemeInit.astro @@ -1,6 +1,19 @@ diff --git a/apps/site/src/lib/remark-strip-duplicate-title.mjs b/apps/site/src/lib/remark-strip-duplicate-title.mjs index 200e6762..9113799c 100644 --- a/apps/site/src/lib/remark-strip-duplicate-title.mjs +++ b/apps/site/src/lib/remark-strip-duplicate-title.mjs @@ -98,6 +98,12 @@ const escapeAttribute = (value) => .replace(/"/g, """) .replace(/ + String(value) + .replace(/&/g, "&") + .replace(//g, ">"); + const encodeCodeAttribute = (value) => encodeURIComponent(String(value)); const playgroundDocs = new Set([ @@ -143,7 +149,9 @@ const getPlaygroundHtml = (id, code, title) => { id )}" data-code="${escapeAttribute( encodeCodeAttribute(code) - )}"${titleAttribute}>
Loading chart playground
`; + )}"${titleAttribute}>
${escapeHtml(
+    code
+  )}
`; }; const isRenderableChartExample = (node, docsPath) => diff --git a/apps/site/src/previews/ChartPlayground.tsx b/apps/site/src/previews/ChartPlayground.tsx index d2c8f313..d4c4e48e 100644 --- a/apps/site/src/previews/ChartPlayground.tsx +++ b/apps/site/src/previews/ChartPlayground.tsx @@ -31,9 +31,16 @@ import { } from "react-native-chart-kit/v2"; import { G, Line as SvgLine, Rect, Text as SvgText } from "react-native-svg"; +import { + chartThemeChangeEvent, + getCurrentChartThemePreset, + type ChartThemePreset +} from "./chartTheme"; import { acquisitionShare, clampChartWidth, + contributionEndDate, + contributionNumDays, contributionValues, money, monthRevenue, @@ -49,9 +56,59 @@ import { import { chartPreviewExamples } from "./registry"; import { showcaseCustomPresets } from "./showcaseTheme"; +type LiveEditorTheme = NonNullable< + React.ComponentProps["theme"] +>; + const importStatementPattern = /^\s*import(?:[\s\S]*?\sfrom\s+["'][^"']+["']|(?:\s+type)?\s+["'][^"']+["']);?\s*/gm; +const explicitThemePattern = + /\b(?:ChartKitProvider|createChartPreset)\b|\b(?:preset|theme)\s*=/; + +const explicitPalettePattern = /\b(?:colorKey|colors)\s*=|\bcolor\s*:/; + +const chartKitLightEditorTheme: LiveEditorTheme = { + plain: { + backgroundColor: "transparent", + color: "#071733" + }, + styles: [ + { + types: ["comment", "prolog", "cdata"], + style: { color: "#64748b", fontStyle: "italic" } + }, + { + types: ["punctuation", "operator"], + style: { color: "#334155" } + }, + { + types: ["keyword", "boolean", "constant"], + style: { color: "#7e22ce", fontWeight: "700" } + }, + { + types: ["string", "char", "attr-value", "regex"], + style: { color: "#047857" } + }, + { + types: ["number", "builtin", "class-name"], + style: { color: "#b45309" } + }, + { + types: ["function", "method"], + style: { color: "#1d4ed8" } + }, + { + types: ["tag", "selector", "property", "symbol"], + style: { color: "#be123c" } + }, + { + types: ["attr-name", "variable"], + style: { color: "#0f766e" } + } + ] +}; + const getThemeMode = (): Exclude => document.documentElement.dataset.theme === "light" ? "light" : "dark"; @@ -129,7 +186,7 @@ const decodeInitialCode = (value: string) => { } }; -const DEFAULT_EDITOR_SIZE = 52; +const DEFAULT_EDITOR_SIZE = 50; const MIN_EDITOR_SIZE = 30; const MAX_EDITOR_SIZE = 70; @@ -139,8 +196,8 @@ const clamp = (value: number, min: number, max: number) => const barPlaygroundData = signups.map((row) => ({ ...row, newCustomers: row.signups, - organic: row.signups, - paid: row.expansion, + organic: row.organic, + paid: row.paid, spend: row.signups, week: row.month })); @@ -178,20 +235,24 @@ const acquisitionSharePlaygroundData = acquisitionShare.map((row) => ({ share: row.value })); -const weeklySpend = Array.from({ length: 14 }, (_, index) => ({ - spend: 24 + Math.round(Math.sin(index / 2) * 8 + index * 2.4), +const weeklySpend = [ + 18, 52, 26, 74, 31, 88, 43, 96, 39, 108, 57, 121, 44, 132 +].map((spend, index) => ({ + spend, week: `W${index + 1}` })); const weeklyAcquisition = weeklySpend.map((row, index) => ({ ...row, - organic: 32 + index * 4, - paid: 18 + Math.round(Math.cos(index / 2) * 6 + index * 2) + organic: [28, 74, 39, 96, 54, 118, 63, 132, 71, 148, 84, 156, 92, 171][ + index + ]!, + paid: [62, 34, 88, 41, 103, 58, 117, 49, 126, 66, 139, 73, 144, 81][index]! })); const portfolioHistory = Array.from({ length: 120 }, (_, index) => { - const portfolio = 84000 + index * 620 + Math.sin(index / 5) * 4200; - const benchmark = 81000 + index * 520 + Math.cos(index / 7) * 3200; + const portfolio = 62000 + index * 780 + Math.sin(index / 2.1) * 15000; + const benchmark = 98000 - index * 170 + Math.cos(index / 2.8) * 12000; return { benchmark, @@ -205,7 +266,8 @@ const portfolioHistory = Array.from({ length: 120 }, (_, index) => { const largeData = portfolioHistory.map((row, index) => ({ ...row, - price: 120 + index * 1.8 + Math.sin(index / 4) * 16 + price: + 120 + index * 1.2 + Math.sin(index / 1.7) * 38 + Math.cos(index / 5) * 22 })); const retentionSegments = [ @@ -271,6 +333,17 @@ const CopyIcon = () => ( ); +const CheckIcon = () => ( + +); + const CodePaneHeader = ({ codeToCopy }: { codeToCopy: string }) => { const [copied, setCopied] = useState(false); const copiedResetRef = useRef(undefined); @@ -312,7 +385,7 @@ const CodePaneHeader = ({ codeToCopy }: { codeToCopy: string }) => { }} type="button" > - + {copied ? : } {copied ? "Copied!" : "Copy to clipboard"} @@ -344,6 +417,9 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { const frameRef = useRef(null); const gridRef = useRef(null); const previewPaneRef = useRef(null); + const [chartThemePreset, setChartThemePreset] = useState( + () => getCurrentChartThemePreset() + ); const [mode, setMode] = useState>(() => getThemeMode() ); @@ -353,6 +429,12 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { const initialCode = useMemo(() => decodeInitialCode(code), [code]); const [currentCode, setCurrentCode] = useState(() => initialCode); const example = chartPreviewExamples[id]; + const supportsGlobalChartTheme = useMemo( + () => + !explicitThemePattern.test(initialCode) && + !explicitPalettePattern.test(initialCode), + [initialCode] + ); useEffect(() => { const previewPane = previewPaneRef.current; @@ -382,6 +464,23 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { return () => themeObserver.disconnect(); }, []); + useEffect(() => { + const updateChartThemePreset = () => + setChartThemePreset(getCurrentChartThemePreset()); + const chartThemeObserver = new MutationObserver(updateChartThemePreset); + chartThemeObserver.observe(document.documentElement, { + attributeFilter: ["data-chart-theme"] + }); + + window.addEventListener(chartThemeChangeEvent, updateChartThemePreset); + updateChartThemePreset(); + + return () => { + chartThemeObserver.disconnect(); + window.removeEventListener(chartThemeChangeEvent, updateChartThemePreset); + }; + }, []); + const scope = useMemo( () => ({ AreaChart, @@ -403,6 +502,9 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { acquisitionShare: acquisitionSharePlaygroundData, clampChartWidth, contributionValues, + contributionEndDate, + contributionNumDays, + chartThemePreset, createChartPreset, data: getPreviewData(id), largeData, @@ -433,7 +535,7 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { weeklyAcquisition, weeklySpend }), - [id, width] + [chartThemePreset, id, width] ); const playgroundStyle = useMemo( @@ -462,6 +564,8 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => { ); }, []); + const previewPreset = supportsGlobalChartTheme ? chartThemePreset : "default"; + return (
{ language="tsx" noInline scope={scope} + theme={mode === "light" ? chartKitLightEditorTheme : undefined} transformCode={prepareLiveCode} >
{
diff --git a/apps/site/src/previews/ChartPreview.tsx b/apps/site/src/previews/ChartPreview.tsx index 0b00b3e7..387ffc8a 100644 --- a/apps/site/src/previews/ChartPreview.tsx +++ b/apps/site/src/previews/ChartPreview.tsx @@ -1,5 +1,10 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; +import { + chartThemeChangeEvent, + getCurrentChartThemePreset, + type ChartThemePreset +} from "./chartTheme"; import { renderChartPreview } from "./examples"; const getThemeMode = (): "dark" | "light" => @@ -7,6 +12,9 @@ const getThemeMode = (): "dark" | "light" => export const ChartPreview = ({ id }: { id: string }) => { const frameRef = useRef(null); + const [chartThemePreset, setChartThemePreset] = useState( + () => getCurrentChartThemePreset() + ); const [mode, setMode] = useState<"dark" | "light">(() => getThemeMode()); const [width, setWidth] = useState(460); @@ -38,9 +46,26 @@ export const ChartPreview = ({ id }: { id: string }) => { return () => themeObserver.disconnect(); }, []); + useEffect(() => { + const updateChartThemePreset = () => + setChartThemePreset(getCurrentChartThemePreset()); + const chartThemeObserver = new MutationObserver(updateChartThemePreset); + chartThemeObserver.observe(document.documentElement, { + attributeFilter: ["data-chart-theme"] + }); + + window.addEventListener(chartThemeChangeEvent, updateChartThemePreset); + updateChartThemePreset(); + + return () => { + chartThemeObserver.disconnect(); + window.removeEventListener(chartThemeChangeEvent, updateChartThemePreset); + }; + }, []); + const preview = useMemo( - () => renderChartPreview({ id, mode, width }), - [id, mode, width] + () => renderChartPreview({ chartThemePreset, id, mode, width }), + [chartThemePreset, id, mode, width] ); return ( diff --git a/apps/site/src/previews/chartTheme.ts b/apps/site/src/previews/chartTheme.ts new file mode 100644 index 00000000..d6de2e6c --- /dev/null +++ b/apps/site/src/previews/chartTheme.ts @@ -0,0 +1,82 @@ +export type ChartThemePreset = + | "default" + | "analytics" + | "fintech" + | "health" + | "ios" + | "material" + | "minimal" + | "highContrast" + | "darkFintech" + | "studio"; + +export const chartThemeStorageKey = "chartkit-chart-theme"; +export const chartThemeChangeEvent = "chartkit:chart-theme-change"; + +export const chartThemeOptions: Array<{ + label: string; + value: ChartThemePreset; +}> = [ + { label: "Default", value: "default" }, + { label: "Analytics", value: "analytics" }, + { label: "Fintech", value: "fintech" }, + { label: "Health", value: "health" }, + { label: "iOS", value: "ios" }, + { label: "Material", value: "material" }, + { label: "Minimal", value: "minimal" }, + { label: "High contrast", value: "highContrast" }, + { label: "Dark fintech", value: "darkFintech" }, + { label: "Studio", value: "studio" } +]; + +export const isChartThemePreset = ( + value: string | null | undefined +): value is ChartThemePreset => + chartThemeOptions.some((option) => option.value === value); + +export const getCurrentChartThemePreset = (): ChartThemePreset => { + if (typeof document !== "undefined") { + const documentTheme = document.documentElement.dataset.chartTheme; + + if (isChartThemePreset(documentTheme)) { + return documentTheme; + } + } + + if (typeof localStorage !== "undefined") { + try { + const storedTheme = localStorage.getItem(chartThemeStorageKey); + + if (isChartThemePreset(storedTheme)) { + return storedTheme; + } + } catch { + // Storage can be blocked in restricted browser contexts. + } + } + + return "default"; +}; + +export const applyChartThemePreset = ( + preset: ChartThemePreset, + options: { dispatch?: boolean } = {} +) => { + if (typeof document !== "undefined") { + document.documentElement.dataset.chartTheme = preset; + } + + if (typeof localStorage !== "undefined") { + try { + localStorage.setItem(chartThemeStorageKey, preset); + } catch { + // Persisting the preference is best-effort. + } + } + + if (options.dispatch !== false && typeof window !== "undefined") { + window.dispatchEvent( + new CustomEvent(chartThemeChangeEvent, { detail: { preset } }) + ); + } +}; diff --git a/apps/site/src/previews/data.ts b/apps/site/src/previews/data.ts index f20ab92a..7ad14b69 100644 --- a/apps/site/src/previews/data.ts +++ b/apps/site/src/previews/data.ts @@ -7,72 +7,95 @@ export const clampChartWidth = (width: number, max = 460) => Math.max(280, Math.min(max, Math.round(width))); export const monthRevenue = [ - { month: "Jan", revenue: 42, forecast: 45, retention: 91 }, - { month: "Feb", revenue: 56, forecast: 54, retention: 94 }, - { month: "Mar", revenue: 61, forecast: 63, retention: 96 }, - { month: "Apr", revenue: 75, forecast: 70, retention: 99 }, - { month: "May", revenue: 83, forecast: 82, retention: 101 }, - { month: "Jun", revenue: 96, forecast: 90, retention: 104 } + { month: "Jan", revenue: 52, forecast: 236, retention: 62 }, + { month: "Feb", revenue: 86, forecast: 218, retention: 138 }, + { month: "Mar", revenue: 58, forecast: 201, retention: 74 }, + { month: "Apr", revenue: 134, forecast: 182, retention: 151 }, + { month: "May", revenue: 95, forecast: 164, retention: 89 }, + { month: "Jun", revenue: 176, forecast: 148, retention: 122 }, + { month: "Jul", revenue: 126, forecast: 132, retention: 55 }, + { month: "Aug", revenue: 218, forecast: 116, retention: 164 }, + { month: "Sep", revenue: 164, forecast: 101, retention: 96 }, + { month: "Oct", revenue: 252, forecast: 88, retention: 145 }, + { month: "Nov", revenue: 198, forecast: 73, retention: 78 }, + { month: "Dec", revenue: 286, forecast: 62, retention: 172 } ]; export const signups = [ - { month: "Jan", signups: 320, expansion: 98 }, - { month: "Feb", signups: 410, expansion: 120 }, - { month: "Mar", signups: 365, expansion: 134 }, - { month: "Apr", signups: 520, expansion: 172 }, - { month: "May", signups: 610, expansion: 198 }, - { month: "Jun", signups: 690, expansion: 230 } + { month: "Jan", signups: 180, expansion: 60, organic: 28, paid: 62 }, + { month: "Feb", signups: 520, expansion: 210, organic: 74, paid: 34 }, + { month: "Mar", signups: 260, expansion: 120, organic: 39, paid: 88 }, + { month: "Apr", signups: 740, expansion: 330, organic: 96, paid: 41 }, + { month: "May", signups: 390, expansion: 170, organic: 54, paid: 103 }, + { month: "Jun", signups: 860, expansion: 410, organic: 118, paid: 58 } ]; export const profit = [ - { month: "Jan", profit: -18 }, - { month: "Feb", profit: -6 }, - { month: "Mar", profit: 12 }, - { month: "Apr", profit: 28 }, - { month: "May", profit: 43 }, - { month: "Jun", profit: 35 } + { month: "Jan", profit: 38 }, + { month: "Feb", profit: -28 }, + { month: "Mar", profit: 64 }, + { month: "Apr", profit: -42 }, + { month: "May", profit: 81 }, + { month: "Jun", profit: -18 } ]; export const supportVolume = [ - { channel: "Email", tickets: 42 }, - { channel: "Chat", tickets: 88 }, - { channel: "Voice", tickets: 36 }, - { channel: "Soc", tickets: 62 } + { channel: "Chat", tickets: 95 }, + { channel: "Email", tickets: 37 }, + { channel: "Phone", tickets: 68 }, + { channel: "Social", tickets: 24 }, + { channel: "Community", tickets: 113 } ]; export const platformShare = [ - { month: "Jan", ios: 44, android: 38, web: 18 }, - { month: "Feb", ios: 42, android: 40, web: 18 }, - { month: "Mar", ios: 45, android: 37, web: 18 }, - { month: "Apr", ios: 48, android: 34, web: 18 } + { month: "Jan", ios: 62, android: 25, web: 13 }, + { month: "Feb", ios: 38, android: 47, web: 15 }, + { month: "Mar", ios: 55, android: 21, web: 24 }, + { month: "Apr", ios: 29, android: 58, web: 13 }, + { month: "May", ios: 68, android: 19, web: 13 }, + { month: "Jun", ios: 44, android: 31, web: 25 } ]; export const acquisitionShare = [ - { channel: "Organic", value: 44, color: "#00163f" }, - { channel: "Paid", value: 28, color: "#2f5f9f" }, - { channel: "Referral", value: 18, color: "#6f88aa" }, - { channel: "Partner", value: 10, color: "#46566f" } + { channel: "Organic search", value: 42, color: "#00163f" }, + { channel: "Paid social", value: 24, color: "#2f5f9f" }, + { channel: "Referrals", value: 18, color: "#6f88aa" }, + { channel: "Partners", value: 10, color: "#46566f" }, + { channel: "Lifecycle", value: 6, color: "#9aa8bd" } ]; export const revenueMix = [ - { label: "Core", value: 58, color: "#00163f" }, - { label: "Expansion", value: 27, color: "#2f5f9f" }, - { label: "Services", value: 15, color: "#6f88aa" } + { label: "Enterprise", value: 680, color: "#00163f" }, + { label: "Business", value: 420, color: "#2f5f9f" }, + { label: "Teams", value: 260, color: "#6f88aa" }, + { label: "Starter", value: 140, color: "#9aa8bd" } ]; export const progressRings = [ - { label: "Activation", value: 0.74, color: "#00163f" }, - { label: "Retention", value: 0.62, color: "#2f5f9f" }, - { label: "Expansion", value: 0.48, color: "#6f88aa" } + { label: "Build signed", value: 0.76, color: "#00163f" }, + { label: "QA pass", value: 0, color: "#2f5f9f" }, + { label: "Rollout cap", value: 0.42, color: "#6f88aa" } ]; -export const contributionValues = Array.from({ length: 84 }, (_, index) => { - const date = new Date(Date.UTC(2026, 0, 1 + index)); - const wave = Math.sin(index / 4) + Math.cos(index / 9); - const count = Math.max(0, Math.round((wave + 1.6) * 4)); +export const contributionEndDate = "2026-05-03"; +export const contributionNumDays = 154; - return { - date: date.toISOString().slice(0, 10), - count - }; -}); +export const contributionValues = Array.from( + { length: contributionNumDays }, + (_, index) => { + const end = new Date(`${contributionEndDate}T00:00:00.000Z`); + const date = new Date( + end.valueOf() - (contributionNumDays - 1 - index) * 24 * 60 * 60 * 1000 + ); + const weekday = date.getUTCDay(); + const cycle = (index * 7 + weekday * 3) % 17; + const launchWeekBoost = index > 110 && index < 126 ? 8 : 0; + const weekendDip = weekday === 0 || weekday === 6 ? -4 : 0; + const count = Math.max(0, cycle + launchWeekBoost + weekendDip); + + return { + date: date.toISOString().slice(0, 10), + count + }; + } +); diff --git a/apps/site/src/previews/examples.tsx b/apps/site/src/previews/examples.tsx index d4eb5fd0..4bd82dfd 100644 --- a/apps/site/src/previews/examples.tsx +++ b/apps/site/src/previews/examples.tsx @@ -3,6 +3,7 @@ import { Text, View } from "react-native"; import { ChartKitProvider, + type CartesianChartPresetValue, type ChartKitThemeMode } from "react-native-chart-kit/v2"; @@ -10,6 +11,7 @@ import { chartPreviewExamples } from "./registry"; import { showcaseCustomPresets } from "./showcaseTheme"; type PreviewRenderContext = { + chartThemePreset: CartesianChartPresetValue; mode: Exclude; width: number; }; @@ -18,10 +20,12 @@ export type ChartPreviewExample = { eyebrow: string; id: string; render: (context: PreviewRenderContext) => ReactNode; + supportsChartTheme?: boolean; title: string; }; export const renderChartPreview = ({ + chartThemePreset, id, mode, width @@ -36,10 +40,13 @@ export const renderChartPreview = ({ ); } + const preset = + example.supportsChartTheme === false ? "default" : chartThemePreset; + return ( @@ -55,7 +62,7 @@ export const renderChartPreview = ({ - {example.render({ mode, width: width - 2 })} + {example.render({ chartThemePreset, mode, width: width - 2 })} diff --git a/apps/site/src/previews/reactNativeWebStub.tsx b/apps/site/src/previews/reactNativeWebStub.tsx index 7fc07bdc..7adc0286 100644 --- a/apps/site/src/previews/reactNativeWebStub.tsx +++ b/apps/site/src/previews/reactNativeWebStub.tsx @@ -7,6 +7,13 @@ type AnyProps = Record; type ScrollHandle = { scrollTo: (options?: { animated?: boolean; x?: number; y?: number }) => void; }; +type ScrollDragState = { + active: boolean; + pointerId: number; + scrollLeft: number; + startX: number; + startY: number; +}; type ResponderEvent = { currentTarget: EventTarget & Element; nativeEvent: { @@ -71,10 +78,17 @@ type ActiveResponder = kind: "responder"; pointerId: number; }; +type PendingResponderPointer = { + pointerId: number; + startClientX: number; + startClientY: number; + startTime: number; +}; type MutableRef = { current: T; }; type PressableState = { focused: boolean; hovered: boolean; pressed: boolean }; +type PointerEventsValue = "auto" | "box-none" | "box-only" | "none"; type StyleValue = | AnyProps | false @@ -92,6 +106,7 @@ const responderProps = new Set([ "keyboardShouldPersistTaps", "nestedScrollEnabled", "onMoveShouldSetPanResponder", + "onMoveShouldSetPanResponderCapture", "onMoveShouldSetResponder", "onMoveShouldSetResponderCapture", "onPanResponderGrant", @@ -105,6 +120,7 @@ const responderProps = new Set([ "onResponderTerminationRequest", "onShouldBlockNativeResponder", "onStartShouldSetPanResponder", + "onStartShouldSetPanResponderCapture", "onStartShouldSetResponder", "onStartShouldSetResponderCapture", "numberOfLines", @@ -236,10 +252,26 @@ const shouldUseResponder = ( phase === "start" ? (props.onStartShouldSetPanResponder as PanResponderPredicate) : (props.onMoveShouldSetPanResponder as PanResponderPredicate); + const panCapturePredicate = + phase === "start" + ? (props.onStartShouldSetPanResponderCapture as PanResponderPredicate) + : (props.onMoveShouldSetPanResponderCapture as PanResponderPredicate); const responderPredicate = phase === "start" ? (props.onStartShouldSetResponder as ResponderPredicate) : (props.onMoveShouldSetResponder as ResponderPredicate); + const responderCapturePredicate = + phase === "start" + ? (props.onStartShouldSetResponderCapture as ResponderPredicate) + : (props.onMoveShouldSetResponderCapture as ResponderPredicate); + + if (panCapturePredicate?.(event, gestureState)) { + return "pan"; + } + + if (responderCapturePredicate?.(event)) { + return "responder"; + } if (panPredicate?.(event, gestureState)) { return "pan"; @@ -255,7 +287,8 @@ const shouldUseResponder = ( const addResponderDomHandlers = ( sourceProps: AnyProps, domProps: AnyProps, - activeResponderRef: MutableRef + activeResponderRef: MutableRef, + pendingPointerRef: MutableRef ) => { const hasResponderProps = Array.from(responderProps).some( (key) => sourceProps[key] !== undefined @@ -344,13 +377,21 @@ const addResponderDomHandlers = ( return; } + const pendingPointer = { + pointerId: event.pointerId, + startClientX: event.clientX, + startClientY: event.clientY, + startTime: Date.now() + }; + pendingPointerRef.current = pendingPointer; + const responderEvent = createResponderEvent(event); const gestureState = createGestureState({ clientX: event.clientX, clientY: event.clientY, - startClientX: event.clientX, - startClientY: event.clientY, - startTime: Date.now(), + startClientX: pendingPointer.startClientX, + startClientY: pendingPointer.startClientY, + startTime: pendingPointer.startTime, stateID: event.pointerId }); const responderKind = shouldUseResponder( @@ -371,14 +412,25 @@ const addResponderDomHandlers = ( const activeResponder = activeResponderRef.current; if (!activeResponder) { + if (event.buttons === 0) { + pendingPointerRef.current = undefined; + return; + } + + const pendingPointer = pendingPointerRef.current; + + if (!pendingPointer || pendingPointer.pointerId !== event.pointerId) { + return; + } + const responderEvent = createResponderEvent(event); const gestureState = createGestureState({ clientX: event.clientX, clientY: event.clientY, - startClientX: event.clientX, - startClientY: event.clientY, - startTime: Date.now(), - stateID: event.pointerId + startClientX: pendingPointer.startClientX, + startClientY: pendingPointer.startClientY, + startTime: pendingPointer.startTime, + stateID: pendingPointer.pointerId }); const responderKind = shouldUseResponder( sourceProps, @@ -427,12 +479,20 @@ const addResponderDomHandlers = ( domProps.onPointerUp = (event: React.PointerEvent) => { (sourceProps.onPointerUp as React.PointerEventHandler)?.(event); finishResponder(event); + + if (pendingPointerRef.current?.pointerId === event.pointerId) { + pendingPointerRef.current = undefined; + } }; domProps.onPointerCancel = (event: React.PointerEvent) => { (sourceProps.onPointerCancel as React.PointerEventHandler)?.( event ); finishResponder(event, true); + + if (pendingPointerRef.current?.pointerId === event.pointerId) { + pendingPointerRef.current = undefined; + } }; }; @@ -475,6 +535,16 @@ const flattenStyle = (style: StyleValue): AnyProps | undefined => { return undefined; }; +const getCssPointerEvents = (pointerEvents: PointerEventsValue | undefined) => { + if (pointerEvents === "none" || pointerEvents === "box-none") { + return "none"; + } + + return pointerEvents === "box-only" || pointerEvents === "auto" + ? "auto" + : undefined; +}; + const toDomProps = (props: AnyProps = {}) => { const { accessibilityLabel, @@ -483,6 +553,7 @@ const toDomProps = (props: AnyProps = {}) => { collapsable: _collapsable, hitSlop: _hitSlop, nativeID, + pointerEvents, pressRetentionOffset: _pressRetentionOffset, style, testID, @@ -514,7 +585,14 @@ const toDomProps = (props: AnyProps = {}) => { domProps["data-testid"] = testID; } - domProps.style = flattenStyle(style as StyleValue); + const flatStyle = flattenStyle(style as StyleValue); + const cssPointerEvents = getCssPointerEvents( + pointerEvents as PointerEventsValue | undefined + ); + domProps.style = { + ...(flatStyle ?? {}), + ...(cssPointerEvents ? { pointerEvents: cssPointerEvents } : {}) + }; return domProps; }; @@ -527,9 +605,17 @@ const createPrimitive = ( const activeResponderRef = React.useRef( undefined ); + const pendingPointerRef = React.useRef( + undefined + ); const domProps = toDomProps(props); domProps.style = { ...defaultStyle, ...(domProps.style as AnyProps) }; - addResponderDomHandlers(props, domProps, activeResponderRef); + addResponderDomHandlers( + props, + domProps, + activeResponderRef, + pendingPointerRef + ); addTouchDomHandlers(props, domProps); return React.createElement(tag, { ...domProps, ref }); @@ -589,11 +675,13 @@ export const ScrollView = React.forwardRef( ref ) => { const elementRef = React.useRef(null); + const scrollDragRef = React.useRef(undefined); const handleScroll = onScroll as | ((event: { nativeEvent: { contentOffset: { x: number; y: number } }; }) => void) | undefined; + const domProps = toDomProps({ ...props, style }); React.useImperativeHandle(ref, () => ({ scrollTo: ({ animated: _animated, x = 0, y = 0 } = {}) => { @@ -604,7 +692,90 @@ export const ScrollView = React.forwardRef( return React.createElement( "div", { - ...toDomProps({ ...props, style }), + ...domProps, + onPointerCancel: (event: React.PointerEvent) => { + ( + domProps.onPointerCancel as + | React.PointerEventHandler + | undefined + )?.(event); + + if (scrollDragRef.current?.pointerId === event.pointerId) { + event.currentTarget.releasePointerCapture?.(event.pointerId); + scrollDragRef.current = undefined; + } + }, + onPointerDown: (event: React.PointerEvent) => { + ( + domProps.onPointerDown as + | React.PointerEventHandler + | undefined + )?.(event); + + if ( + !horizontal || + event.button !== 0 || + event.defaultPrevented || + event.currentTarget.scrollWidth <= event.currentTarget.clientWidth + ) { + return; + } + + scrollDragRef.current = { + active: false, + pointerId: event.pointerId, + scrollLeft: event.currentTarget.scrollLeft, + startX: event.clientX, + startY: event.clientY + }; + event.currentTarget.setPointerCapture?.(event.pointerId); + }, + onPointerMove: (event: React.PointerEvent) => { + ( + domProps.onPointerMove as + | React.PointerEventHandler + | undefined + )?.(event); + + const drag = scrollDragRef.current; + + if (!drag || drag.pointerId !== event.pointerId) { + return; + } + + const deltaX = event.clientX - drag.startX; + const deltaY = event.clientY - drag.startY; + const absoluteDeltaX = Math.abs(deltaX); + const absoluteDeltaY = Math.abs(deltaY); + + if (!drag.active) { + if (absoluteDeltaX < 3) { + return; + } + + if (absoluteDeltaY > absoluteDeltaX) { + event.currentTarget.releasePointerCapture?.(event.pointerId); + scrollDragRef.current = undefined; + return; + } + } + + drag.active = true; + event.currentTarget.scrollLeft = drag.scrollLeft - deltaX; + event.preventDefault(); + }, + onPointerUp: (event: React.PointerEvent) => { + ( + domProps.onPointerUp as + | React.PointerEventHandler + | undefined + )?.(event); + + if (scrollDragRef.current?.pointerId === event.pointerId) { + event.currentTarget.releasePointerCapture?.(event.pointerId); + scrollDragRef.current = undefined; + } + }, onScroll: (event: React.UIEvent) => { handleScroll?.({ nativeEvent: { @@ -617,9 +788,9 @@ export const ScrollView = React.forwardRef( }, ref: elementRef, style: { + ...(domProps.style as AnyProps), overflowX: horizontal ? "auto" : "hidden", - overflowY: horizontal ? "hidden" : "auto", - ...(flattenStyle(style as StyleValue) ?? {}) + overflowY: horizontal ? "hidden" : "auto" } }, React.createElement( diff --git a/apps/site/src/previews/registry.tsx b/apps/site/src/previews/registry.tsx index 53b7ce95..a98ffe67 100644 --- a/apps/site/src/previews/registry.tsx +++ b/apps/site/src/previews/registry.tsx @@ -14,6 +14,8 @@ import type { ChartPreviewExample } from "./examples"; import { acquisitionShare, clampChartWidth, + contributionEndDate, + contributionNumDays, contributionValues, money, monthRevenue, @@ -207,7 +209,7 @@ export const chartPreviewExamples: Record = { title: "Revenue mix", render: ({ width }) => ( = { "progress-rings": { eyebrow: "Progress", id: "progress-rings", - title: "Activation health", + title: "Release checklist", render: ({ width }) => ( `${Math.round(average * 100)}%`} @@ -241,7 +243,7 @@ export const chartPreviewExamples: Record = { render: ({ width }) => ( = { render: ({ width }) => ( @@ -270,9 +272,9 @@ export const chartPreviewExamples: Record = { title: "No activity", render: ({ width }) => ( diff --git a/apps/site/src/styles/starlight.css b/apps/site/src/styles/starlight.css index c57ab088..a46c73f5 100644 --- a/apps/site/src/styles/starlight.css +++ b/apps/site/src/styles/starlight.css @@ -125,6 +125,128 @@ button[data-open-modal] { border-color: var(--sl-color-hairline); } +.chartkit-chart-theme-control { + display: inline-flex; + align-items: center; + gap: 0.45rem; + margin-inline-end: 0.25rem; + color: var(--sl-color-gray-3); + font-size: 0.72rem; + font-weight: 700; + line-height: 1; + white-space: nowrap; +} + +.chartkit-chart-theme-control__label { + color: inherit; +} + +.chartkit-chart-theme-control__select { + height: 2rem; + min-width: 8.6rem; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.55rem; + background: rgba(255, 255, 255, 0.035); + color: var(--sl-color-white); + cursor: pointer; + font: inherit; + font-size: 0.74rem; + font-weight: 650; + line-height: 1; + padding: 0 0.65rem; +} + +.chartkit-chart-theme-control__select:hover, +.chartkit-chart-theme-control__select:focus { + border-color: rgba(255, 255, 255, 0.18); + color: var(--sl-color-white); +} + +.chartkit-chart-theme-control__select:focus-visible { + outline: 2px solid rgba(120, 168, 255, 0.55); + outline-offset: 2px; +} + +.chartkit-chart-theme-control__select option { + background: var(--sl-color-bg); + color: var(--sl-color-white); +} + +:root[data-theme="light"] .chartkit-chart-theme-control { + color: rgba(7, 23, 51, 0.48); +} + +:root[data-theme="light"] .chartkit-chart-theme-control__select { + background: rgba(7, 23, 51, 0.035); + color: #071733; +} + +:root[data-theme="light"] .chartkit-chart-theme-control__select:hover, +:root[data-theme="light"] .chartkit-chart-theme-control__select:focus { + border-color: rgba(7, 23, 51, 0.18); + color: #071733; +} + +:root[data-theme="light"] .chartkit-chart-theme-control__select option { + background: #ffffff; + color: #071733; +} + +starlight-theme-select { + display: inline-flex; + align-items: center; + justify-content: center; +} + +starlight-theme-select > label { + display: none; +} + +.chartkit-docs-theme-toggle { + display: inline-flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + border: 0; + border-radius: 0.45rem; + background: transparent; + color: var(--sl-color-gray-3); + cursor: pointer; + padding: 0; + transition: color 0.15s ease; +} + +.chartkit-docs-theme-toggle:hover, +.chartkit-docs-theme-toggle:focus-visible { + color: var(--sl-color-white); +} + +.chartkit-docs-theme-toggle:focus-visible { + outline: 2px solid rgba(120, 168, 255, 0.55); + outline-offset: 2px; +} + +.chartkit-docs-theme-toggle svg { + display: block; + width: 1.05rem; + height: 1.05rem; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.25; +} + +:root[data-theme="light"] .chartkit-docs-theme-toggle { + color: rgba(7, 23, 51, 0.48); +} + +:root[data-theme="light"] .chartkit-docs-theme-toggle:hover, +:root[data-theme="light"] .chartkit-docs-theme-toggle:focus-visible { + color: #071733; +} + @media (min-width: 50rem) { .sidebar-pane { background: var(--sl-color-bg-sidebar); @@ -213,7 +335,7 @@ button[data-open-modal] { } .content-panel:first-child { - padding-top: 2.65rem; + padding-top: 1.45rem; } @media (min-width: 72rem) { @@ -265,15 +387,16 @@ button[data-open-modal] { color: var(--sl-color-text); } +h1#_top, .sl-markdown-content h1:not(:where(.not-content *)) { - max-width: 16ch; - font-size: 3.1rem; + max-width: 20ch; + font-size: 2.45rem; font-weight: 760; } .sl-markdown-content h2:not(:where(.not-content *)) { - padding-top: 0.35rem; - font-size: 2rem; + padding-top: 0.15rem; + font-size: 1.7rem; font-weight: 760; } @@ -319,11 +442,201 @@ button[data-open-modal] { border-radius: 0.8rem; } +.expressive-code .copy { + --chart-kit-code-copy-tooltip: "Copy to clipboard"; + --chart-kit-code-copy-icon: var(--ec-frm-copyIcon); + --chart-kit-code-check-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2.4'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M20%206%209%2017l-5-5'%2F%3E%3C%2Fsvg%3E"); +} + +.expressive-code .copy:has(.feedback.show) { + --chart-kit-code-copy-tooltip: "Copied!"; +} + +.expressive-code .copy::before { + position: absolute; + z-index: 4; + right: 0; + top: calc(100% + 0.5rem); + width: max-content; + max-width: 11rem; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.45rem; + background: #161a23; + color: var(--sl-color-white); + content: var(--chart-kit-code-copy-tooltip); + font-size: 0.72rem; + font-weight: 650; + line-height: 1; + opacity: 0; + padding: 0.45rem 0.55rem; + pointer-events: none; + transform: translateY(-0.2rem); + transition: + opacity 0.12s ease, + transform 0.12s ease; + visibility: hidden; +} + +.expressive-code .copy::after { + position: absolute; + z-index: 5; + right: 0.65rem; + top: calc(100% + 0.2rem); + width: 0.5rem; + height: 0.5rem; + border-top: 1px solid var(--sl-color-hairline); + border-left: 1px solid var(--sl-color-hairline); + background: #161a23; + content: ""; + opacity: 0; + pointer-events: none; + transform: translateY(-0.2rem) rotate(45deg); + transition: + opacity 0.12s ease, + transform 0.12s ease; + visibility: hidden; +} + +:root[data-theme="light"] .expressive-code .copy::before, +:root[data-theme="light"] .expressive-code .copy::after { + background: #ffffff; + color: #071733; +} + +.expressive-code .copy:has(button:hover)::before, +.expressive-code .copy:has(button:focus-visible)::before, +.expressive-code .copy:has(.feedback.show)::before, +.expressive-code .copy:has(button:hover)::after, +.expressive-code .copy:has(button:focus-visible)::after, +.expressive-code .copy:has(.feedback.show)::after { + opacity: 1; + transform: translateY(0); + visibility: visible; +} + +.expressive-code .copy:has(button:hover)::after, +.expressive-code .copy:has(button:focus-visible)::after, +.expressive-code .copy:has(.feedback.show)::after { + transform: translateY(0) rotate(45deg); +} + +.expressive-code .copy button { + width: 1.7rem; + height: 1.7rem; + border: 0; + border-radius: 0.45rem; + background: transparent; + color: var(--sl-color-gray-3); + opacity: 1; + transition-property: opacity, background-color, color; +} + +.expressive-code .copy button:hover, +.expressive-code .copy button:focus-visible, +.expressive-code .copy:has(.feedback.show) button { + background: rgba(255, 255, 255, 0.08); + color: var(--sl-color-white); + opacity: 1; +} + +:root[data-theme="light"] .expressive-code .copy button:hover, +:root[data-theme="light"] .expressive-code .copy button:focus-visible, +:root[data-theme="light"] .expressive-code .copy:has(.feedback.show) button { + background: rgba(7, 23, 51, 0.08); + color: #071733; +} + +.expressive-code .copy button:focus-visible { + outline: 2px solid rgba(120, 168, 255, 0.55); + outline-offset: 2px; +} + +.expressive-code .copy button div, +.expressive-code .copy button::before { + display: none; +} + +.expressive-code .copy button::after { + inset: auto; + top: 50%; + left: 50%; + width: 0.86rem; + height: 0.86rem; + margin: 0; + background-color: currentColor; + -webkit-mask-image: var(--chart-kit-code-copy-icon); + mask-image: var(--chart-kit-code-copy-icon); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 100% 100%; + mask-size: 100% 100%; + transform: translate(-50%, -50%); +} + +.expressive-code .copy:has(.feedback.show) button::after { + -webkit-mask-image: var(--chart-kit-code-check-icon); + mask-image: var(--chart-kit-code-check-icon); +} + +.expressive-code .copy .feedback { + position: absolute; + overflow: hidden; + width: 1px; + height: 1px; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} + +@media (hover: hover) { + .expressive-code .copy button { + opacity: 0; + } + + .expressive-code .frame:hover .copy button:not(:hover), + .expressive-code .frame:focus-within .copy button:not(:hover), + .expressive-code .copy:has(.feedback.show) button:not(:hover) { + opacity: 0.82; + } + + .expressive-code .copy button:hover, + .expressive-code .copy button:focus-visible { + opacity: 1; + } +} + chart-kit-playground { display: block; margin-block: 1.25rem 2rem; } +chart-kit-playground .chart-kit-preview-fallback { + overflow: hidden; + border: 1px solid var(--sl-color-hairline); + border-radius: 0.9rem; + background: #11141c; +} + +:root[data-theme="light"] chart-kit-playground .chart-kit-preview-fallback { + background: #f8fbff; +} + +chart-kit-playground .chart-kit-preview-fallback pre { + margin: 0; + overflow: auto; + color: #e8edf8; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.79rem; + line-height: 1.65; + padding: 1rem; + tab-size: 2; +} + +:root[data-theme="light"] chart-kit-playground .chart-kit-preview-fallback pre { + color: #071733; +} + .chart-kit-playground__frame { overflow: hidden; border: 1px solid var(--sl-color-hairline); @@ -347,7 +660,7 @@ chart-kit-playground { .chart-kit-playground__grid { display: grid; grid-template-columns: - minmax(18rem, var(--chart-kit-editor-size, 52%)) 1px + minmax(18rem, var(--chart-kit-editor-size, 50%)) 1px minmax(18rem, 1fr); min-height: 28rem; } @@ -592,10 +905,49 @@ chart-kit-playground { color: inherit; outline: none; padding: 1rem !important; + scrollbar-color: rgba(122, 167, 255, 0.42) transparent; + scrollbar-width: thin; tab-size: 2; white-space: pre; } +:root[data-theme="light"] .chart-kit-playground__editor pre { + scrollbar-color: rgba(37, 99, 235, 0.34) transparent; +} + +.chart-kit-playground__editor pre::-webkit-scrollbar { + width: 0.55rem; + height: 0.55rem; +} + +.chart-kit-playground__editor pre::-webkit-scrollbar-track, +.chart-kit-playground__editor pre::-webkit-scrollbar-corner { + background: transparent; +} + +.chart-kit-playground__editor pre::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 999px; + background-color: rgba(122, 167, 255, 0.36); + background-clip: content-box; +} + +.chart-kit-playground__editor pre::-webkit-scrollbar-thumb:hover { + background-color: rgba(122, 167, 255, 0.56); +} + +:root[data-theme="light"] + .chart-kit-playground__editor + pre::-webkit-scrollbar-thumb { + background-color: rgba(37, 99, 235, 0.28); +} + +:root[data-theme="light"] + .chart-kit-playground__editor + pre::-webkit-scrollbar-thumb:hover { + background-color: rgba(37, 99, 235, 0.46); +} + .chart-kit-playground__editor pre:focus-visible { outline: 2px solid rgba(120, 168, 255, 0.55); outline-offset: -2px; @@ -647,6 +999,29 @@ chart-kit-playground { padding: 0.22rem 0.4rem; } +.right-sidebar-panel ul ul { + margin-block: 0.05rem 0.25rem; +} + +.right-sidebar-panel ul ul a { + display: flex; + align-items: stretch; + color: var(--sl-color-gray-3); + font-size: 0.78rem; + font-weight: 600; + padding-inline-start: 1.2rem; +} + +.right-sidebar-panel ul ul a::before { + align-self: stretch; + width: 1px; + flex: 0 0 auto; + margin-block: 0.18rem; + margin-inline-end: 0.45rem; + background: var(--sl-color-hairline); + content: ""; +} + .right-sidebar-panel a:hover { background: rgba(255, 255, 255, 0.045); } @@ -659,12 +1034,24 @@ chart-kit-playground { border-color: var(--sl-color-hairline); border-radius: 0.9rem; background: rgba(255, 255, 255, 0.035); + font-size: 0.68rem; + font-weight: 650; + line-height: 1.35; + padding: 0.85rem; } :root[data-theme="light"] .pagination-links a { background: #ffffff; } +.pagination-links .link-title { + display: inline-block; + margin-top: 0.18rem; + font-size: 0.86rem; + font-weight: 700; + line-height: 1.25; +} + .sl-markdown-content blockquote:not(:where(.not-content *)), .sl-markdown-content details:not(:where(.not-content *)) { border-color: var(--sl-color-hairline); @@ -675,6 +1062,20 @@ chart-kit-playground { --sl-content-pad-x: 1.25rem; } + .mobile-preferences .chartkit-chart-theme-control { + width: 100%; + justify-content: space-between; + margin-inline-end: 0; + } + + .mobile-preferences .chartkit-chart-theme-control__select { + min-width: min(11rem, 58vw); + } + + .mobile-preferences starlight-theme-select { + margin-inline-start: auto; + } + .site-title { font-size: 0.86rem; } @@ -684,12 +1085,13 @@ chart-kit-playground { height: 1.85rem; } + h1#_top, .sl-markdown-content h1:not(:where(.not-content *)) { - font-size: 2.2rem; + font-size: 1.9rem; } .sl-markdown-content h2:not(:where(.not-content *)) { - font-size: 1.6rem; + font-size: 1.42rem; } .chart-kit-playground__grid { diff --git a/docs/README.md b/docs/README.md index d5460d1b..4d5bdf58 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ Modern v2 examples import from `react-native-chart-kit/v2`. ## Getting Started - [Quickstart](getting-started/installation.md) +- [Contributing](getting-started/contributing.md) ## Migration @@ -39,14 +40,3 @@ Modern v2 examples import from `react-native-chart-kit/v2`. ## Recipes - [Production recipes](recipes/README.md) - -## Local Review - -Type-check the React Native CLI example surface when changing package imports or peer setup: - -```sh -npm run example:rn-cli:typecheck -``` - -The Expo preview app lives in the private `chart-kit-pro` repository because it -combines free and Pro chart examples. diff --git a/docs/charts/area.md b/docs/charts/area.md index b8891477..42857b08 100644 --- a/docs/charts/area.md +++ b/docs/charts/area.md @@ -15,10 +15,12 @@ area fill. import { AreaChart } from "react-native-chart-kit/v2"; const data = [ - { date: "Jan", price: 18 }, - { date: "Feb", price: 34 }, - { date: "Mar", price: 29 }, - { date: "Apr", price: 52 } + { date: "Jan 01", price: 72 }, + { date: "Jan 03", price: 138 }, + { date: "Jan 08", price: 91 }, + { date: "Jan 15", price: 166 }, + { date: "Jan 24", price: 118 }, + { date: "Feb 01", price: 202 } ]; export function PipelineChart() { @@ -43,6 +45,15 @@ Use `areaFill` to tune fill opacity. You can also pass `area` to `LineChart` when a product uses line and area variants from the same component wrapper. ```tsx +const data = [ + { date: "Jan", price: 42 }, + { date: "Feb", price: 96 }, + { date: "Mar", price: 58 }, + { date: "Apr", price: 132 }, + { date: "May", price: 84 }, + { date: "Jun", price: 158 } +]; + +/>; ``` Use `areaFill` on the chart for a shared fill style, or per series when each @@ -64,6 +75,21 @@ Threshold coloring clips the rendered path and area fill above or below a y value. Raw points are unchanged. ```tsx +const data = [ + { month: "Jan", attainment: 62 }, + { month: "Feb", attainment: 138 }, + { month: "Mar", attainment: 74 }, + { month: "Apr", attainment: 151 }, + { month: "May", attainment: 89 }, + { month: "Jun", attainment: 122 }, + { month: "Jul", attainment: 55 }, + { month: "Aug", attainment: 164 }, + { month: "Sep", attainment: 96 }, + { month: "Oct", attainment: 145 }, + { month: "Nov", attainment: 78 }, + { month: "Dec", attainment: 172 } +]; + +/>; ``` ## Dense Area Charts @@ -107,3 +133,65 @@ as `LineChart`. height={260} /> ``` + +## Props + +### AreaChart + +`AreaChart` accepts the same prop surface as `LineChart` and renders with area fill enabled by default. + +| Prop | Type | Description | +| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `data` | `TData[]` | Object-row source data for the chart. | +| `xKey` | `keyof TData` | Row key used for the x-axis value. | +| `yKey` | `keyof TData` | Single row key used for y values when `series` or `yKeys` is not provided. | +| `yKeys` | `Array` | Multiple row keys rendered as separate series with default styling. | +| `series` | `LineChartSeries[]` | Full per-series configuration, including labels, colors, stroke, dots, thresholds, and area fill. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `ChartKitThemeMode` or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `scrollable` | `boolean` | Enables a horizontal scroll viewport for long data sets. | +| `visiblePoints` | `number` | Number of points visible in the viewport when `scrollable` is enabled. | +| `initialIndex` | `ChartViewportInitialIndex` | Initial scroll/window position, such as `"start"` or `"end"`. | +| `viewport` | `LineChartViewportConfig` | Controlled visible data window for scroll, pan, zoom, or range selector flows. | +| `onViewportChange` | `(event) => void` | Called when viewport state changes from the main plot or range selector. | +| `viewportInteraction` | `boolean` or `LineChartViewportInteractionConfig` | Enables and configures one-finger pan and pinch zoom for the viewport. | +| `rangeSelector` | `boolean` or `LineChartRangeSelectorConfig` | Adds a mini-chart range selector below the main plot. | +| `decimation` | `false`, `"auto"`, `number`, or `LineChartDecimationConfig` | Controls rendered path simplification for dense series. | +| `curve` | `LineCurve` | Curve interpolation used for line and area paths. | +| `connectNulls` | `boolean` | Connects defined points across `null` or missing y values. | +| `area` | `boolean` | Inherited from `LineChartProps`; `AreaChart` renders with area fill enabled. | +| `areaFill` | `LineChartAreaFillConfig` | Shared area fill opacity, gradient, or color configuration. | +| `showDots` | `boolean` | Shows or hides all point markers. | +| `dots` | `boolean` or `LineChartDotConfig` | Configures default point marker visibility, size, shape, and color. | +| `renderDot` | `(props) => ReactNode` | Custom renderer for ordinary point markers. | +| `selectedIndex` | `number` | Controlled selected data index. | +| `defaultSelectedIndex` | `number` | Initial uncontrolled selected data index. | +| `activeDot` | `boolean` or `LineChartDotConfig` | Configures the marker shown for the selected point. | +| `renderActiveDot` | `(props) => ReactNode` | Custom renderer for the selected point marker. | +| `interaction` | `LineChartInteraction` | Selection/scrub interaction mode and callbacks. | +| `crosshair` | `boolean` or `LineChartCrosshairConfig` | Shows and configures the selected-point crosshair. | +| `renderCrosshair` | `(props) => ReactNode` | Custom renderer for the crosshair. | +| `tooltip` | `boolean` or `LineChartTooltipConfig` | Shows and configures selected-point tooltip content and placement. | +| `renderTooltip` | `(props) => ReactNode` | Custom renderer for selected-point tooltip content. | +| `referenceLines` | `LineChartReferenceLineConfig[]` | Horizontal reference lines drawn across the plot. | +| `referenceBands` | `LineChartReferenceBandConfig[]` | Horizontal reference bands drawn behind the series. | +| `showHorizontalGridLines` | `boolean` | Shows or hides horizontal grid lines. | +| `showVerticalGridLines` | `boolean` | Shows or hides vertical grid lines. | +| `legend` | `boolean` or `LineChartLegendConfig` | Shows and configures the chart legend. | +| `labelStrategy` | `LineChartLabelStrategy` | Controls x-axis label density and layout. | +| `labelRotation` | `number` | Rotation angle for x-axis labels when using rotated labels. | +| `labelMinGap` | `number` | Minimum gap used by automatic x-axis label skipping. | +| `edgeLabelPolicy` | `LineChartEdgeLabelPolicy` | Controls how first and last x-axis labels are shifted, hidden, or shown. | +| `yDomain` | `NumericDomainInput` | Overrides or constrains the computed y-axis domain. | +| `yAxisLabelWidth` | `LineChartYAxisLabelWidth` | Fixed, automatic, or stable width for y-axis labels. | +| `axisLabelAnimation` | `boolean` or `LineChartAxisLabelAnimationConfig` | Animates y-axis label changes during viewport updates. | +| `formatXLabel` | `(value, index) => string` | Formats x-axis labels and selected x labels. | +| `formatYLabel` | `(value) => string` | Formats y-axis labels, selected values, and tooltip values. | +| `renderer` | `LineChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `id` | `string` | Stable chart id used for internal ids and coordinated selection scope. | +| `debugLayout` | `boolean` | Renders layout debug rectangles in development. | +| `onLayoutDebug` | `(model) => void` | Receives computed layout debug geometry. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | diff --git a/docs/charts/bar.md b/docs/charts/bar.md index 79ceb5ce..a4d64e88 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -17,9 +17,12 @@ All examples on this page are part of the public free v2 API. import { BarChart } from "react-native-chart-kit/v2"; const data = [ - { month: "Jan", signups: 42 }, - { month: "Feb", signups: 48 }, - { month: "Mar", signups: 54 } + { month: "Jan", signups: 180 }, + { month: "Feb", signups: 520 }, + { month: "Mar", signups: 260 }, + { month: "Apr", signups: 740 }, + { month: "May", signups: 390 }, + { month: "Jun", signups: 860 } ]; export function SignupsChart() { @@ -42,6 +45,15 @@ export function SignupsChart() { Use `series` for multiple bars per x value. The chart shows a bottom legend by default when there is more than one series. ```tsx +const data = [ + { month: "Jan", organic: 28, paid: 62 }, + { month: "Feb", organic: 74, paid: 34 }, + { month: "Mar", organic: 39, paid: 88 }, + { month: "Apr", organic: 96, paid: 41 }, + { month: "May", organic: 54, paid: 103 }, + { month: "Jun", organic: 118, paid: 58 } +]; + +/>; ``` ::chart-preview{id="bar-grouped"} @@ -70,6 +82,15 @@ Useful grouped-bar props: Negative values render below the zero baseline. Keep `yDomain` on its default include-zero behavior unless you intentionally want a cropped baseline. ```tsx +const profit = [ + { month: "Jan", profit: 38 }, + { month: "Feb", profit: -28 }, + { month: "Mar", profit: 64 }, + { month: "Apr", profit: -42 }, + { month: "May", profit: 81 }, + { month: "Jun", profit: -18 } +]; + (value < 0 ? `-$${Math.abs(value)}k` : `$${value}k`)} width={360} height={250} -/> +/>; ``` ::chart-preview{id="bar-negative"} @@ -89,6 +110,14 @@ Negative values render below the zero baseline. Keep `yDomain` on its default in Set `orientation="horizontal"` when category labels are easier to scan down the left axis. ```tsx +const supportVolume = [ + { channel: "Chat", tickets: 95 }, + { channel: "Email", tickets: 37 }, + { channel: "Phone", tickets: 68 }, + { channel: "Social", tickets: 24 }, + { channel: "Community", tickets: 113 } +]; + +/>; ``` ::chart-preview{id="bar-horizontal"} @@ -109,6 +138,15 @@ Horizontal bars support grouped, stacked, 100% stacked, and negative values thro Set `mode="stacked"` to stack series by row. Positive and negative stacks are tracked separately from the zero baseline. ```tsx +const data = [ + { month: "Jan", newCustomers: 180, expansion: 60 }, + { month: "Feb", newCustomers: 520, expansion: 210 }, + { month: "Mar", newCustomers: 260, expansion: 120 }, + { month: "Apr", newCustomers: 740, expansion: 330 }, + { month: "May", newCustomers: 390, expansion: 170 }, + { month: "Jun", newCustomers: 860, expansion: 410 } +]; + +/>; ``` ## 100% Stacked Bars @@ -127,18 +165,28 @@ Set `mode="stacked"` to stack series by row. Positive and negative stacks are tr Set `mode="stacked100"` for percentage composition. The original values are preserved in the bar model while rendered heights are normalized to row totals. ```tsx +const platformShare = [ + { month: "Jan", ios: 62, android: 25, web: 13 }, + { month: "Feb", ios: 38, android: 47, web: 15 }, + { month: "Mar", ios: 55, android: 21, web: 24 }, + { month: "Apr", ios: 29, android: 58, web: 13 }, + { month: "May", ios: 68, android: 19, web: 13 }, + { month: "Jun", ios: 44, android: 31, web: 25 } +]; + `${value}%`} width={360} height={250} -/> +/>; ``` ::chart-preview{id="bar-stacked"} @@ -180,6 +228,15 @@ The facade maps `labels`, `legend`, `data`, `barColors`, `hideLegend`, `percenti Bar selection is opt-in. Use `interaction="tap"` for the simplest behavior, or pass an object when you need callbacks or outside-press dismissal. ```tsx +const data = [ + { month: "Jan", organic: 28, paid: 62 }, + { month: "Feb", organic: 74, paid: 34 }, + { month: "Mar", organic: 39, paid: 88 }, + { month: "Apr", organic: 96, paid: 41 }, + { month: "May", organic: 54, paid: 103 }, + { month: "Jun", organic: 118, paid: 58 } +]; + +/>; ``` The public select event includes `dataIndex`, `seriesKey`, `seriesLabel`, `value`, `formattedValue`, `x`, `xLabel`, `color`, `position`, and the original `raw` row. @@ -214,6 +271,27 @@ Tooltip positioning uses `anchor: "bar"` and `placement: "auto"` by default. Use Use `scrollable`, `visiblePoints`, and `initialIndex` for long categorical bar charts. For bars, `visiblePoints` maps to visible bar bands. ```tsx +const weeklySpend = [ + { week: "W1", spend: 18 }, + { week: "W2", spend: 52 }, + { week: "W3", spend: 26 }, + { week: "W4", spend: 74 }, + { week: "W5", spend: 31 }, + { week: "W6", spend: 88 }, + { week: "W7", spend: 43 }, + { week: "W8", spend: 96 }, + { week: "W9", spend: 39 }, + { week: "W10", spend: 108 }, + { week: "W11", spend: 57 }, + { week: "W12", spend: 121 }, + { week: "W13", spend: 44 }, + { week: "W14", spend: 132 }, + { week: "W15", spend: 63 }, + { week: "W16", spend: 118 }, + { week: "W17", spend: 71 }, + { week: "W18", spend: 146 } +]; + `$${value}k`} width={360} height={250} -/> +/>; ``` The y-axis labels stay pinned while the bars and x-axis labels scroll horizontally. @@ -236,6 +314,17 @@ Use `renderBar` when bars need product-specific styling while keeping the built- ```tsx import { Rect } from "react-native-svg"; +const weeklySpend = [ + { week: "W1", spend: 18 }, + { week: "W2", spend: 52 }, + { week: "W3", spend: 26 }, + { week: "W4", spend: 74 }, + { week: "W5", spend: 31 }, + { week: "W6", spend: 88 }, + { week: "W7", spend: 43 }, + { week: "W8", spend: 96 } +]; + ` | Multiple row keys rendered as grouped or stacked series with default styling. | +| `series` | `BarChartSeries[]` | Full per-series configuration, including y key, label, key, and color. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `ChartKitThemeMode` or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `scrollable` | `boolean` | Enables a horizontal scroll viewport for long bar sets. | +| `visiblePoints` | `number` | Number of x values visible in the viewport when `scrollable` is enabled. | +| `initialIndex` | `ChartViewportInitialIndex` | Initial scroll position, such as `"start"` or `"end"`. | +| `orientation` | `BarChartOrientation` | Renders vertical or horizontal bars. | +| `mode` | `BarChartMode` | Chooses grouped, stacked, or 100% stacked layout. | +| `yDomain` | `NumericDomainInput` | Overrides or constrains the computed value-axis domain. | +| `barRadius` | `number` | Corner radius applied to rendered bars. | +| `barWidthRatio` | `number` | Portion of each category slot occupied by bars. | +| `barGapRatio` | `number` | Relative gap between grouped bars. | +| `showValuesOnTopOfBars` | `boolean` | Shows formatted value labels at bar ends. | +| `showHorizontalGridLines` | `boolean` | Shows or hides horizontal grid lines. | +| `showXAxisLabels` | `boolean` | Shows or hides x-axis labels. | +| `showYAxisLabels` | `boolean` | Shows or hides y-axis labels. | +| `yTickCount` | `number` | Number of ticks used for value-axis labels and grid lines. | +| `legend` | `boolean` | Shows or hides the bottom legend. | +| `interaction` | `BarChartInteraction` | Tap selection mode and callbacks. | +| `selectedBar` | `BarChartSelectedBar` | Controlled selected bar by data index and series key. | +| `defaultSelectedBar` | `BarChartSelectedBar` | Initial uncontrolled selected bar. | +| `selectionAnimation` | `boolean` or `BarChartSelectionAnimationConfig` | Enables and configures selected-bar animation. | +| `tooltip` | `boolean` or `BarChartTooltipConfig` | Shows and configures selected-bar tooltip content and placement. | +| `renderBar` | `(props) => ReactNode` | Custom renderer for each bar while preserving layout and hit testing. | +| `renderer` | `BarChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `labelStrategy` | `BarChartLabelStrategy` | Controls x-axis label visibility. | +| `formatXLabel` | `(value, index) => string` | Formats x-axis labels and selected x labels. | +| `formatYLabel` | `(value) => string` | Formats y-axis labels, value labels, and tooltip values. | +| `id` | `string` | Stable chart id used for coordinated selection scope. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | + +### StackedBarChart + +`StackedBarChart` is the v2 compatibility facade for legacy stacked-bar data. + +| Prop | Type | Description | +| -------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | +| `data` | `StackedBarChartLegacyData` | Legacy stacked-bar data with `labels`, `legend`, `data`, and `barColors`. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `chartConfig` | `StackedBarChartLegacyConfig` | Legacy color, label, decimal, bar, and grid configuration. | +| `hideLegend` | `boolean` | Hides the generated legend when true. | +| `style` | `StyleProp` | Style applied to the compatibility wrapper. | +| `barPercentage` | `number` | Overrides the bar width ratio from `chartConfig`. | +| `decimalPlaces` | `number` | Controls formatted value precision. | +| `withVerticalLabels` | `boolean` | Shows or hides x-axis labels. | +| `withHorizontalLabels` | `boolean` | Shows or hides y-axis labels. | +| `withInnerLines` | `boolean` | Shows or hides inner grid lines. | +| `segments` | `number` | Controls y-axis tick density. | +| `percentile` | `boolean` | Renders the stack as 100% stacked values. | +| `fromZero` | `boolean` | Forces the value domain to include zero. | +| `yAxisLabel` | `string` | Prefix added to formatted y-axis values. | +| `yAxisSuffix` | `string` | Suffix added to formatted y-axis values. | +| `verticalLabelsHeightPercentage` | `number` | Legacy compatibility prop accepted by the facade. | +| `formatYLabel` | `(yLabel) => string` | Formats y-axis labels after decimal formatting. | +| `theme` | `ChartKitThemeMode` or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | diff --git a/docs/charts/contribution-heatmap.md b/docs/charts/contribution-heatmap.md index 7bd98130..4ac803fb 100644 --- a/docs/charts/contribution-heatmap.md +++ b/docs/charts/contribution-heatmap.md @@ -10,13 +10,28 @@ description: Render calendar-style contribution heatmaps for activity and intens ```tsx import { ContributionGraph } from "react-native-chart-kit/v2"; +const endDate = "2026-05-03"; +const numDays = 154; +const values = Array.from({ length: numDays }, (_, index) => { + const end = new Date(`${endDate}T00:00:00.000Z`); + const date = new Date( + end.valueOf() - (numDays - 1 - index) * 24 * 60 * 60 * 1000 + ); + const weekday = date.getUTCDay(); + const cycle = (index * 7 + weekday * 3) % 17; + const launchWeekBoost = index > 110 && index < 126 ? 8 : 0; + const weekendDip = weekday === 0 || weekday === 6 ? -4 : 0; + + return { + date: date.toISOString().slice(0, 10), + count: Math.max(0, cycle + launchWeekBoost + weekendDip) + }; +}); + string` | Custom color resolver for each rendered cell. | +| `getMonthLabel` | `(monthIndex, date) => string` | Custom month label formatter. | +| `getWeekdayLabel` | `(dayIndex) => string` | Custom weekday label formatter. | +| `onDayPress` | `(event) => void` | Called when a day cell is pressed. | +| `renderer` | `ContributionGraphRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | diff --git a/docs/charts/donut.md b/docs/charts/donut.md index 19069f1a..07c736ff 100644 --- a/docs/charts/donut.md +++ b/docs/charts/donut.md @@ -13,6 +13,13 @@ inner radius and center-label support. ```tsx import { DonutChart } from "react-native-chart-kit/v2"; +const revenueMix = [ + { plan: "Enterprise", revenue: 680 }, + { plan: "Business", revenue: 420 }, + { plan: "Teams", revenue: 260 }, + { plan: "Starter", revenue: 140 } +]; + +/>; ``` + +## Props + +### DonutChart + +`DonutChart` accepts the same prop surface as `PieChart` and defaults `innerRadiusRatio` to `0.58`. + +| Prop | Type | Description | +| ---------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `data` | `TData[]` | Object-row source data for the chart. | +| `valueKey` | `keyof TData` | Row key used for slice values. | +| `labelKey` | `keyof TData` | Row key used for slice and legend labels. | +| `colorKey` | `keyof TData` | Row key used for per-slice colors. | +| `colors` | `string[]` | Fallback color palette used when `colorKey` is not provided. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `"light"`, `"dark"`, `"system"`, or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `innerRadius` | `number` | Explicit inner radius in pixels. | +| `innerRadiusRatio` | `number` | Inner radius as a fraction of the computed outer radius. | +| `legend` | `boolean` or `PieChartLegendConfig` | Shows and configures the wrapped legend. | +| `arcLabels` | `boolean` or `PieChartArcLabelsConfig` | Shows and configures external arc labels and connector lines. | +| `selectedIndex` | `number` | Controlled selected slice index. | +| `defaultSelectedIndex` | `number` | Initial uncontrolled selected slice index. | +| `activeSlice` | `PieChartActiveSliceConfig` | Configures selected-slice stroke, opacity, offset, and scale. | +| `selectionAnimation` | `boolean` or `PieChartSelectionAnimationConfig` | Enables and configures selected-slice animation. | +| `interaction` | `PieChartInteraction` | Tap selection mode and callbacks. | +| `centerLabel` | `string`, `ReactNode`, or `(props) => ReactNode` | Content rendered in the donut center. | +| `renderer` | `PieChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `id` | `string` | Stable chart id used for coordinated selection scope. | +| `testID` | `string` | Test identifier applied to the chart surface. | +| `formatValue` | `(value) => string` | Formats raw slice values in labels and accessible output. | +| `formatPercentage` | `(percentage) => string` | Formats percentage labels in legends, arc labels, and accessible output. | diff --git a/docs/charts/line.md b/docs/charts/line.md index 99e23c20..b7966b89 100644 --- a/docs/charts/line.md +++ b/docs/charts/line.md @@ -17,10 +17,18 @@ handled separately by the compatibility facade. import { LineChart } from "react-native-chart-kit/v2"; const data = [ - { month: "Jan", revenue: 18 }, - { month: "Feb", revenue: 34 }, - { month: "Mar", revenue: 29 }, - { month: "Apr", revenue: 52 } + { month: "Jan", revenue: 52 }, + { month: "Feb", revenue: 86 }, + { month: "Mar", revenue: 58 }, + { month: "Apr", revenue: 134 }, + { month: "May", revenue: 95 }, + { month: "Jun", revenue: 176 }, + { month: "Jul", revenue: 126 }, + { month: "Aug", revenue: 218 }, + { month: "Sep", revenue: 164 }, + { month: "Oct", revenue: 252 }, + { month: "Nov", revenue: 198 }, + { month: "Dec", revenue: 286 } ]; export function RevenueChart() { @@ -44,6 +52,17 @@ Use `series` when each line needs its own label, color, marker, curve, or stroke style. ```tsx +const data = [ + { month: "Jan", actual: 22, forecast: 190 }, + { month: "Feb", actual: 64, forecast: 168 }, + { month: "Mar", actual: 39, forecast: 143 }, + { month: "Apr", actual: 118, forecast: 122 }, + { month: "May", actual: 73, forecast: 101 }, + { month: "Jun", actual: 161, forecast: 84 }, + { month: "Jul", actual: 109, forecast: 66 }, + { month: "Aug", actual: 204, forecast: 48 } +]; + +/>; ``` ::chart-preview{id="line-multi-series"} @@ -70,6 +89,16 @@ stroke style. Supported curve values are `linear`, `monotone`, and `step`. ```tsx +const data = [ + { date: "Mon", portfolio: 512, benchmark: 690 }, + { date: "Tue", portfolio: 660, benchmark: 610 }, + { date: "Wed", portfolio: 430, benchmark: 650 }, + { date: "Thu", portfolio: 760, benchmark: 540 }, + { date: "Fri", portfolio: 590, benchmark: 700 }, + { date: "Sat", portfolio: 820, benchmark: 520 }, + { date: "Sun", portfolio: 548, benchmark: 735 } +]; + +/>; ``` ## Threshold Coloring @@ -107,6 +136,21 @@ Threshold coloring clips the rendered path above or below a y value. Raw points are unchanged. ```tsx +const data = [ + { month: "Jan", attainment: 62 }, + { month: "Feb", attainment: 138 }, + { month: "Mar", attainment: 74 }, + { month: "Apr", attainment: 151 }, + { month: "May", attainment: 89 }, + { month: "Jun", attainment: 122 }, + { month: "Jul", attainment: 55 }, + { month: "Aug", attainment: 164 }, + { month: "Sep", attainment: 96 }, + { month: "Oct", attainment: 145 }, + { month: "Nov", attainment: 78 }, + { month: "Dec", attainment: 172 } +]; + +/>; ``` ## Tooltips and Selection @@ -133,6 +177,17 @@ Selection state is shared by tooltips, active dots, crosshairs, and external UI through `onSelect`. ```tsx +const data = [ + { date: "Nov 03", portfolio: 512, benchmark: 720 }, + { date: "Nov 04", portfolio: 681, benchmark: 604 }, + { date: "Nov 05", portfolio: 438, benchmark: 698 }, + { date: "Nov 10", portfolio: 794, benchmark: 552 }, + { date: "Nov 11", portfolio: 566, benchmark: 746 }, + { date: "Nov 12", portfolio: 842, benchmark: 580 }, + { date: "Nov 13", portfolio: 618, benchmark: 790 }, + { date: "Nov 14", portfolio: 906, benchmark: 534 } +]; + +/>; ``` ::chart-preview{id="line-selection"} @@ -186,6 +241,17 @@ custom inspection overlay. ```tsx import { G, Line, Text as SvgText } from "react-native-svg"; +const data = [ + { month: "Jan", actual: 22, forecast: 190 }, + { month: "Feb", actual: 64, forecast: 168 }, + { month: "Mar", actual: 39, forecast: 143 }, + { month: "Apr", actual: 118, forecast: 122 }, + { month: "May", actual: 73, forecast: 101 }, + { month: "Jun", actual: 161, forecast: 84 }, + { month: "Jul", actual: 109, forecast: 66 }, + { month: "Aug", actual: 204, forecast: 48 } +]; + ({ }); +/>; ``` ## Layout Debug @@ -342,3 +423,63 @@ import { `getLineChartDataTable()` returns columns and rows suitable for an app-level table fallback or export workflow. + +## Props + +### LineChart + +| Prop | Type | Description | +| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `data` | `TData[]` | Object-row source data for the chart. | +| `xKey` | `keyof TData` | Row key used for the x-axis value. | +| `yKey` | `keyof TData` | Single row key used for y values when `series` or `yKeys` is not provided. | +| `yKeys` | `Array` | Multiple row keys rendered as separate series with default styling. | +| `series` | `LineChartSeries[]` | Full per-series configuration, including labels, colors, stroke, dots, thresholds, and area fill. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `ChartKitThemeMode` or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `scrollable` | `boolean` | Enables a horizontal scroll viewport for long data sets. | +| `visiblePoints` | `number` | Number of points visible in the viewport when `scrollable` is enabled. | +| `initialIndex` | `ChartViewportInitialIndex` | Initial scroll/window position, such as `"start"` or `"end"`. | +| `viewport` | `LineChartViewportConfig` | Controlled visible data window for scroll, pan, zoom, or range selector flows. | +| `onViewportChange` | `(event) => void` | Called when viewport state changes from the main plot or range selector. | +| `viewportInteraction` | `boolean` or `LineChartViewportInteractionConfig` | Enables and configures one-finger pan and pinch zoom for the viewport. | +| `rangeSelector` | `boolean` or `LineChartRangeSelectorConfig` | Adds a mini-chart range selector below the main plot. | +| `decimation` | `false`, `"auto"`, `number`, or `LineChartDecimationConfig` | Controls rendered path simplification for dense series. | +| `curve` | `LineCurve` | Curve interpolation used for line and area paths. | +| `connectNulls` | `boolean` | Connects defined points across `null` or missing y values. | +| `area` | `boolean` | Renders area fills under the line series. | +| `areaFill` | `LineChartAreaFillConfig` | Shared area fill opacity, gradient, or color configuration. | +| `showDots` | `boolean` | Shows or hides all point markers. | +| `dots` | `boolean` or `LineChartDotConfig` | Configures default point marker visibility, size, shape, and color. | +| `renderDot` | `(props) => ReactNode` | Custom renderer for ordinary point markers. | +| `selectedIndex` | `number` | Controlled selected data index. | +| `defaultSelectedIndex` | `number` | Initial uncontrolled selected data index. | +| `activeDot` | `boolean` or `LineChartDotConfig` | Configures the marker shown for the selected point. | +| `renderActiveDot` | `(props) => ReactNode` | Custom renderer for the selected point marker. | +| `interaction` | `LineChartInteraction` | Selection/scrub interaction mode and callbacks. | +| `crosshair` | `boolean` or `LineChartCrosshairConfig` | Shows and configures the selected-point crosshair. | +| `renderCrosshair` | `(props) => ReactNode` | Custom renderer for the crosshair. | +| `tooltip` | `boolean` or `LineChartTooltipConfig` | Shows and configures selected-point tooltip content and placement. | +| `renderTooltip` | `(props) => ReactNode` | Custom renderer for selected-point tooltip content. | +| `referenceLines` | `LineChartReferenceLineConfig[]` | Horizontal reference lines drawn across the plot. | +| `referenceBands` | `LineChartReferenceBandConfig[]` | Horizontal reference bands drawn behind the series. | +| `showHorizontalGridLines` | `boolean` | Shows or hides horizontal grid lines. | +| `showVerticalGridLines` | `boolean` | Shows or hides vertical grid lines. | +| `legend` | `boolean` or `LineChartLegendConfig` | Shows and configures the chart legend. | +| `labelStrategy` | `LineChartLabelStrategy` | Controls x-axis label density and layout. | +| `labelRotation` | `number` | Rotation angle for x-axis labels when using rotated labels. | +| `labelMinGap` | `number` | Minimum gap used by automatic x-axis label skipping. | +| `edgeLabelPolicy` | `LineChartEdgeLabelPolicy` | Controls how first and last x-axis labels are shifted, hidden, or shown. | +| `yDomain` | `NumericDomainInput` | Overrides or constrains the computed y-axis domain. | +| `yAxisLabelWidth` | `LineChartYAxisLabelWidth` | Fixed, automatic, or stable width for y-axis labels. | +| `axisLabelAnimation` | `boolean` or `LineChartAxisLabelAnimationConfig` | Animates y-axis label changes during viewport updates. | +| `formatXLabel` | `(value, index) => string` | Formats x-axis labels and selected x labels. | +| `formatYLabel` | `(value) => string` | Formats y-axis labels, selected values, and tooltip values. | +| `renderer` | `LineChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `id` | `string` | Stable chart id used for internal ids and coordinated selection scope. | +| `debugLayout` | `boolean` | Renders layout debug rectangles in development. | +| `onLayoutDebug` | `(model) => void` | Receives computed layout debug geometry. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | diff --git a/docs/charts/pie.md b/docs/charts/pie.md index fc8a1d8f..4cf10711 100644 --- a/docs/charts/pie.md +++ b/docs/charts/pie.md @@ -56,6 +56,14 @@ Use `interaction="tap"` for uncontrolled selection, or pass `selectedIndex` with `interaction.onSelect` for controlled product UI. ```tsx +const acquisitionShare = [ + { channel: "Organic search", share: 42 }, + { channel: "Paid social", share: 24 }, + { channel: "Referrals", share: 18 }, + { channel: "Partners", share: 10 }, + { channel: "Lifecycle", share: 6 } +]; + const [selectedIndex, setSelectedIndex] = useState(0); +/>; ``` + +## Props + +### PieChart + +| Prop | Type | Description | +| ---------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `data` | `TData[]` | Object-row source data for the chart. | +| `valueKey` | `keyof TData` | Row key used for slice values. | +| `labelKey` | `keyof TData` | Row key used for slice and legend labels. | +| `colorKey` | `keyof TData` | Row key used for per-slice colors. | +| `colors` | `string[]` | Fallback color palette used when `colorKey` is not provided. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `"light"`, `"dark"`, `"system"`, or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `innerRadius` | `number` | Explicit inner radius in pixels for donut-style rendering. | +| `innerRadiusRatio` | `number` | Inner radius as a fraction of the computed outer radius. | +| `legend` | `boolean` or `PieChartLegendConfig` | Shows and configures the wrapped legend. | +| `arcLabels` | `boolean` or `PieChartArcLabelsConfig` | Shows and configures external arc labels and connector lines. | +| `selectedIndex` | `number` | Controlled selected slice index. | +| `defaultSelectedIndex` | `number` | Initial uncontrolled selected slice index. | +| `activeSlice` | `PieChartActiveSliceConfig` | Configures selected-slice stroke, opacity, offset, and scale. | +| `selectionAnimation` | `boolean` or `PieChartSelectionAnimationConfig` | Enables and configures selected-slice animation. | +| `interaction` | `PieChartInteraction` | Tap selection mode and callbacks. | +| `centerLabel` | `string`, `ReactNode`, or `(props) => ReactNode` | Content rendered in the chart center. | +| `renderer` | `PieChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `id` | `string` | Stable chart id used for coordinated selection scope. | +| `testID` | `string` | Test identifier applied to the chart surface. | +| `formatValue` | `(value) => string` | Formats raw slice values in labels and accessible output. | +| `formatPercentage` | `(percentage) => string` | Formats percentage labels in legends, arc labels, and accessible output. | diff --git a/docs/charts/progress.md b/docs/charts/progress.md index 1ff93bce..c392d093 100644 --- a/docs/charts/progress.md +++ b/docs/charts/progress.md @@ -13,9 +13,9 @@ The v2 progress surface supports concentric rings and single-ring completion sta import { ProgressChart } from "react-native-chart-kit/v2"; const data = [ - { metric: "Move", progress: 0.72, color: "#f43f5e" }, - { metric: "Exercise", progress: 0.48, color: "#22c55e" }, - { metric: "Stand", progress: 0.9, color: "#2563eb" } + { metric: "Build signed", progress: 0.76, color: "#00163f" }, + { metric: "QA pass", progress: 0, color: "#2f5f9f" }, + { metric: "Rollout cap", progress: 0.42, color: "#6f88aa" } ]; ; ``` @@ -53,9 +53,9 @@ The legacy data object still works: ```tsx ``` + +## Props + +### ProgressChart + +| Prop | Type | Description | +| --------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `data` | `ProgressChartData` | Modern object-row data or the legacy progress data object. | +| `valueKey` | `keyof TData` | Row key used for ring progress values in object-row data. | +| `labelKey` | `keyof TData` | Row key used for ring labels in object-row data. | +| `colorKey` | `keyof TData` | Row key used for ring colors in object-row data. | +| `labels` | `string[]` | Labels used with legacy data arrays. | +| `colors` | `string[]` | Colors used with legacy data arrays or as fallback ring colors. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `"light"`, `"dark"`, `"system"`, or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `legend` | `boolean` or `ProgressChartLegendConfig` | Shows and configures the legend. | +| `hideLegend` | `boolean` | Legacy-style shortcut for hiding the legend. | +| `centerLabel` | `string` or `(props) => string` | Text rendered in the center of the rings. | +| `strokeWidth` | `number` | Ring stroke width in pixels. | +| `ringGap` | `number` | Gap between concentric rings. | +| `radius` | `number` | Explicit outer ring radius in pixels. | +| `animation` | `boolean` or `ProgressChartAnimationConfig` | Enables and configures ring entrance/update animation. | +| `strokeLinecap` | `ProgressChartStrokeLinecap` | Stroke cap style for progress arcs. | +| `backgroundRingColor` | `string` | Color used for ring tracks behind progress arcs. | +| `renderer` | `ProgressChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | +| `formatPercentage` | `(value) => string` | Formats percentages in labels and accessible output. | + +### ProgressRing + +| Prop | Type | Description | +| --------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `value` | `number`, `null`, or `undefined` | Single progress value for the ring, usually between `0` and `1`. | +| `label` | `string` | Label used for the ring and accessible output. | +| `color` | `string` | Progress arc color for the ring. | +| `width` | `number` | Outer chart width in pixels. | +| `height` | `number` | Outer chart height in pixels. | +| `theme` | `"light"`, `"dark"`, `"system"`, or `CartesianChartTheme` | Theme mode or inline theme tokens for this chart. | +| `preset` | `CartesianChartPresetValue` | Built-in or registered preset name used to seed chart colors and typography. | +| `legend` | `boolean` or `ProgressChartLegendConfig` | Shows and configures the legend. | +| `hideLegend` | `boolean` | Legacy-style shortcut for hiding the legend. | +| `centerLabel` | `string` or `(props) => string` | Text rendered in the ring center. | +| `strokeWidth` | `number` | Ring stroke width in pixels. | +| `ringGap` | `number` | Gap setting inherited from `ProgressChart`; only relevant when the component is wrapped or extended. | +| `radius` | `number` | Explicit ring radius in pixels. | +| `animation` | `boolean` or `ProgressChartAnimationConfig` | Enables and configures ring entrance/update animation. | +| `strokeLinecap` | `ProgressChartStrokeLinecap` | Stroke cap style for the progress arc. | +| `backgroundRingColor` | `string` | Color used for the track behind the progress arc. | +| `renderer` | `ProgressChartRenderer` | Renderer implementation used for SVG-compatible primitives. | +| `accessibilityLabel` | `string` | Overrides the generated accessible chart summary. | +| `testID` | `string` | Test identifier applied to the chart surface. | +| `formatPercentage` | `(value) => string` | Formats percentages in labels and accessible output. | diff --git a/docs/charts/themes.md b/docs/charts/themes.md index 21cac82c..f0de278e 100644 --- a/docs/charts/themes.md +++ b/docs/charts/themes.md @@ -24,6 +24,15 @@ const acme = createChartPreset({ } }); +const data = [ + { date: "Jan", revenue: 52 }, + { date: "Feb", revenue: 86 }, + { date: "Mar", revenue: 58 }, + { date: "Apr", revenue: 134 }, + { date: "May", revenue: 95 }, + { date: "Jun", revenue: 176 } +]; + ; @@ -44,3 +53,16 @@ Built-in presets include: - `darkFintech` or `dark-fintech` Use `theme` for one-off chart overrides and `createChartPreset()` for design-system presets that should be shared across a product. + +## Props + +### ChartKitProvider + +| Prop | Type | Description | +| ---------- | ------------------------------ | ------------------------------------------------------------------ | +| `children` | `ReactNode` | Chart subtree that should inherit provider defaults. | +| `mode` | `ChartKitThemeMode` | Theme mode to apply, including `"light"`, `"dark"`, or `"system"`. | +| `preset` | `CartesianChartPresetValue` | Default preset name or preset object used by descendant charts. | +| `presets` | `CartesianChartPresetRegistry` | Custom preset registry available to descendant charts. | +| `renderer` | `LineChartRenderer` | Default SVG-compatible renderer used by descendant chart surfaces. | +| `theme` | `CartesianChartTheme` | Inline theme overrides merged into descendant chart themes. | diff --git a/docs/getting-started/contributing.md b/docs/getting-started/contributing.md new file mode 100644 index 00000000..542d3bfd --- /dev/null +++ b/docs/getting-started/contributing.md @@ -0,0 +1,83 @@ +--- +title: Contributing +description: Set up the React Native Chart Kit repository and run local development checks. +--- + +# Contributing + +This page is for developing React Native Chart Kit itself. If you are installing +the library in an app, start with the [Quickstart](installation.md). + +## Repository Setup + +```sh +git clone git@github.com:indiespirit/react-native-chart-kit.git +cd react-native-chart-kit +npm install +``` + +The repository uses npm workspaces and `package-lock.json`. The published npm +package is `react-native-chart-kit`; the `@chart-kit/*` workspaces are internal +packages used to develop and build the modern implementation. + +## Local Checks + +Run the focused check for the area you changed, then run the broader checks +before opening a pull request. + +```sh +npm run lint +npm run typecheck +npm run test +npm run docs:build +npm run build +``` + +Useful focused commands: + +| Command | Purpose | +| ---------------------------------- | -------------------------------------------------------- | +| `npm run core:typecheck` | Type-check the v2 core workspace. | +| `npm run svg:typecheck` | Type-check the SVG renderer workspace. | +| `npm run rn:typecheck` | Type-check the React Native workspace. | +| `npm run test:unit` | Run unit tests. | +| `npm run test:compat` | Run legacy compatibility tests. | +| `npm run example:rn-cli:typecheck` | Type-check the React Native CLI smoke example. | +| `npm run docs:build` | Verify docs links and type-check documentation examples. | +| `npm run pack:check` | Dry-run package artifacts for the public package. | +| `npm run surface:check` | Check public exports and package boundaries. | + +## Example App + +The public repo includes a React Native CLI smoke surface for non-Expo import and +peer-dependency checks: + +```sh +npm run example:rn-cli:typecheck +``` + +See `examples/rn-cli-basic` for the app source and Metro aliases. + +The Expo preview app lives in the private `chart-kit-pro` repository because it +combines free and Pro chart examples. + +## Branch Names + +Use lowercase kebab-case branch names in this format: + +```sh +/ +``` + +Use these branch types: + +- `fix/` for user-visible bug fixes +- `feat/` for new public functionality +- `docs/` for documentation-only changes +- `ci/` for automation and CI changes +- `chore/` for maintenance, release hygiene, and dependency upkeep +- `refactor/` for internal changes that do not intentionally change behavior +- `release/` for version bump and publishing prep + +Include an issue number when the branch maps to a specific issue, for example +`fix/733-svg-gradient-ids`. Keep each branch scoped to one pull request. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 4991f111..58b88873 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -5,48 +5,49 @@ description: Install React Native Chart Kit and render a first chart. # Quickstart -Chart Kit v2 is developed in private repo-internal `@chart-kit/*` workspaces, -but `react-native-chart-kit` is the only public npm install path. +Install the public package in your React Native app. Do not install +`@chart-kit/*` packages directly; those are repo-internal workspaces. -For local development in this repository: +## React Native CLI ```sh -git clone git@github.com:indiespirit/react-native-chart-kit.git -cd react-native-chart-kit -npm install +npm install react-native-chart-kit react-native-svg ``` -For apps: +For iOS apps, install native pods after installing dependencies: ```sh -npm install react-native-chart-kit react-native-svg +cd ios +pod install ``` -The root import remains the legacy-compatible API. The modern free v2 API is -available from the `react-native-chart-kit/v2` subpath. - ## Expo -For Expo apps, install the native peer dependencies with Expo so versions match the SDK: +Install the package and the Expo-compatible `react-native-svg` version: ```sh +npm install react-native-chart-kit npx expo install react-native-svg ``` -Baseline tap, scrub, pan, pinch zoom, and range-selector interactions use React Native responder APIs, so new apps do not need a gesture-handler wrapper just to render or inspect charts. +Baseline tap, scrub, pan, pinch zoom, and range-selector interactions use React +Native responder APIs, so new apps do not need a gesture-handler wrapper just to +render or inspect charts. ## First Modern Chart -New v2 screens can import the modern API from the public package subpath. +New screens can import the modern v2 API from the public package subpath. ```tsx import { LineChart } from "react-native-chart-kit/v2"; const data = [ - { month: "Jan", revenue: 18 }, - { month: "Feb", revenue: 34 }, - { month: "Mar", revenue: 29 }, - { month: "Apr", revenue: 52 } + { month: "Jan", revenue: 52 }, + { month: "Feb", revenue: 86 }, + { month: "Mar", revenue: 58 }, + { month: "Apr", revenue: 134 }, + { month: "May", revenue: 95 }, + { month: "Jun", revenue: 176 } ]; export function RevenueChart() { @@ -64,17 +65,3 @@ export function RevenueChart() { ``` The root import remains the legacy-compatible surface for existing screens. - -## Local Review App - -This public repo includes a React Native CLI smoke surface for non-Expo import -and peer-dependency checks: - -```sh -npm run example:rn-cli:typecheck -``` - -See `examples/rn-cli-basic` for the app source and Metro aliases. - -The Expo preview app lives in the private `chart-kit-pro` repository because it -combines free and Pro chart examples. diff --git a/docs/migration/from-v1.md b/docs/migration/from-v1.md index 38044367..ec00e44a 100644 --- a/docs/migration/from-v1.md +++ b/docs/migration/from-v1.md @@ -50,9 +50,12 @@ import { LineChart } from "react-native-chart-kit/v2"; setSelectedDay(event)} width={360} diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 4eaa7a57..0bf5ca8e 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -25,9 +25,11 @@ Modern docs: - [Docs home](../../docs/README.md) - [Installation](../../docs/getting-started/installation.md) -- [Line and area charts](../../docs/charts/line-and-area.md) +- [Line charts](../../docs/charts/line.md) +- [Area charts](../../docs/charts/area.md) - [Bar charts](../../docs/charts/bar.md) -- [Pie and donut charts](../../docs/charts/pie-and-donut.md) +- [Pie charts](../../docs/charts/pie.md) +- [Donut charts](../../docs/charts/donut.md) - [Progress charts](../../docs/charts/progress.md) - [Contribution heatmap](../../docs/charts/contribution-heatmap.md) - [Migration from v1](../../docs/migration/from-v1.md) diff --git a/packages/react-native/src/charts/contribution/ContributionGraph.tsx b/packages/react-native/src/charts/contribution/ContributionGraph.tsx index 523a58ca..bfc76721 100644 --- a/packages/react-native/src/charts/contribution/ContributionGraph.tsx +++ b/packages/react-native/src/charts/contribution/ContributionGraph.tsx @@ -163,6 +163,7 @@ export const CalendarHeatmap = ContributionGraph; const styles = StyleSheet.create({ container: { + borderRadius: 8, overflow: "hidden" } }); diff --git a/packages/react-native/src/charts/pie/PieChart.tsx b/packages/react-native/src/charts/pie/PieChart.tsx index df636be3..b3659ec1 100644 --- a/packages/react-native/src/charts/pie/PieChart.tsx +++ b/packages/react-native/src/charts/pie/PieChart.tsx @@ -411,6 +411,7 @@ export const DonutChart = >( const styles = StyleSheet.create({ container: { + borderRadius: 8, overflow: "hidden" }, centerLabelOverlay: { diff --git a/packages/react-native/src/charts/progress/ProgressChart.tsx b/packages/react-native/src/charts/progress/ProgressChart.tsx index aa97abb8..1d100776 100644 --- a/packages/react-native/src/charts/progress/ProgressChart.tsx +++ b/packages/react-native/src/charts/progress/ProgressChart.tsx @@ -237,6 +237,7 @@ export const ProgressRing = ({ const styles = StyleSheet.create({ container: { + borderRadius: 8, overflow: "hidden" }, legend: { diff --git a/scripts/typecheck-doc-examples.mjs b/scripts/typecheck-doc-examples.mjs index bc3e8466..6cddd913 100644 --- a/scripts/typecheck-doc-examples.mjs +++ b/scripts/typecheck-doc-examples.mjs @@ -23,6 +23,7 @@ const publicMarkdownRoots = [ ]; const checkedFenceLanguages = new Set(["ts", "tsx"]); const chartKitNames = [ + "AreaChart", "BarChart", "ChartKitProvider", "ContributionGraph", From fae06b33d8b77a8069200447c2e2de35d5938171 Mon Sep 17 00:00:00 2001 From: iexitdev Date: Fri, 29 May 2026 23:08:38 -0400 Subject: [PATCH 5/7] Add Google Analytics tag to site --- apps/site/src/components/GoogleAnalytics.astro | 15 +++++++++++++++ apps/site/src/components/Head.astro | 2 ++ apps/site/src/pages/index.astro | 2 ++ 3 files changed, 19 insertions(+) create mode 100644 apps/site/src/components/GoogleAnalytics.astro diff --git a/apps/site/src/components/GoogleAnalytics.astro b/apps/site/src/components/GoogleAnalytics.astro new file mode 100644 index 00000000..aa1f9d4c --- /dev/null +++ b/apps/site/src/components/GoogleAnalytics.astro @@ -0,0 +1,15 @@ + + + diff --git a/apps/site/src/components/Head.astro b/apps/site/src/components/Head.astro index 6153d2df..1defa617 100644 --- a/apps/site/src/components/Head.astro +++ b/apps/site/src/components/Head.astro @@ -1,4 +1,5 @@ --- +import GoogleAnalytics from "./GoogleAnalytics.astro"; import ThemeInit from "./ThemeInit.astro"; const { head } = Astro.locals.starlightRoute; @@ -13,6 +14,7 @@ const reactRefreshPreamble = ` --- +