Skip to content

Commit f7d49a4

Browse files
author
iexitdev
committed
Enable editable playgrounds for chart examples
1 parent bb712ed commit f7d49a4

3 files changed

Lines changed: 246 additions & 22 deletions

File tree

apps/site/src/lib/remark-strip-duplicate-title.mjs

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,30 @@ const escapeAttribute = (value) =>
9898
.replace(/"/g, """)
9999
.replace(/</g, "&lt;");
100100

101-
const editablePreviewIds = new Set(["line-basic"]);
102-
103101
const encodeCodeAttribute = (value) => encodeURIComponent(String(value));
104102

103+
const playgroundDocs = new Set([
104+
"getting-started/installation.md",
105+
"recipes/README.md",
106+
"charts/area.md",
107+
"charts/bar.md",
108+
"charts/contribution-heatmap.md",
109+
"charts/donut.md",
110+
"charts/line.md",
111+
"charts/pie.md",
112+
"charts/progress.md"
113+
]);
114+
115+
const chartComponentPattern =
116+
/<\s*(AreaChart|BarChart|ContributionGraph|DonutChart|LineChart|PieChart|ProgressChart|ProgressRing|StackedBarChart)\b/;
117+
118+
const slugify = (value) =>
119+
String(value)
120+
.toLowerCase()
121+
.trim()
122+
.replace(/[^a-z0-9]+/g, "-")
123+
.replace(/^-|-$/g, "");
124+
105125
const getPreviewHtml = (id, title) => {
106126
const titleAttribute =
107127
typeof title === "string" && title.length > 0
@@ -113,11 +133,68 @@ const getPreviewHtml = (id, title) => {
113133
)}"${titleAttribute}><div class="chart-kit-preview-fallback">Loading chart preview</div></chart-kit-preview>`;
114134
};
115135

116-
const transformPreviewDirectives = (tree) => {
136+
const getPlaygroundHtml = (id, code, title) => {
137+
const titleAttribute =
138+
typeof title === "string" && title.length > 0
139+
? ` data-preview-title="${escapeAttribute(title)}"`
140+
: "";
141+
142+
return `<chart-kit-playground data-preview-id="${escapeAttribute(
143+
id
144+
)}" data-code="${escapeAttribute(
145+
encodeCodeAttribute(code)
146+
)}"${titleAttribute}><div class="chart-kit-preview-fallback">Loading chart playground</div></chart-kit-playground>`;
147+
};
148+
149+
const isRenderableChartExample = (node, docsPath) =>
150+
playgroundDocs.has(docsPath) &&
151+
node?.type === "code" &&
152+
["jsx", "tsx"].includes(node.lang) &&
153+
chartComponentPattern.test(node.value ?? "");
154+
155+
const getGeneratedPreviewId = (docsPath, title, index) => {
156+
const pathSlug = slugify(
157+
docsPath.replace(/\/README\.md$/, "").replace(/\.mdx?$/, "")
158+
);
159+
const titleSlug = slugify(title || "example");
160+
161+
return `${pathSlug}-${titleSlug || "example"}-${index}`;
162+
};
163+
164+
const transformPreviewDirectives = (tree, file) => {
165+
const docsPath = getDocsEntryPath(file);
166+
117167
if (Array.isArray(tree.children)) {
168+
let currentHeading = file.data?.astro?.frontmatter?.title ?? "";
169+
let generatedPreviewIndex = 0;
170+
118171
for (let index = 0; index < tree.children.length; index += 1) {
119172
const node = tree.children[index];
120173

174+
if (node.type === "heading" && node.depth >= 2) {
175+
currentHeading = textFromNode(node).trim();
176+
continue;
177+
}
178+
179+
if (
180+
isRenderableChartExample(node, docsPath) &&
181+
tree.children[index + 1]?.type !== "leafDirective"
182+
) {
183+
generatedPreviewIndex += 1;
184+
node.type = "html";
185+
node.value = getPlaygroundHtml(
186+
getGeneratedPreviewId(
187+
docsPath,
188+
currentHeading,
189+
generatedPreviewIndex
190+
),
191+
node.value,
192+
currentHeading
193+
);
194+
node.children = [];
195+
continue;
196+
}
197+
121198
if (node.type !== "leafDirective" || node.name !== "chart-preview") {
122199
continue;
123200
}
@@ -135,22 +212,13 @@ const transformPreviewDirectives = (tree) => {
135212
const previousNode = tree.children[index - 1];
136213

137214
if (
138-
editablePreviewIds.has(id) &&
139215
previousNode?.type === "code" &&
140216
["jsx", "tsx"].includes(previousNode.lang)
141217
) {
142218
const title = node.attributes?.title;
143-
const titleAttribute =
144-
typeof title === "string" && title.length > 0
145-
? ` data-preview-title="${escapeAttribute(title)}"`
146-
: "";
147219

148220
previousNode.type = "html";
149-
previousNode.value = `<chart-kit-playground data-preview-id="${escapeAttribute(
150-
id
151-
)}" data-code="${escapeAttribute(
152-
encodeCodeAttribute(previousNode.value)
153-
)}"${titleAttribute}><div class="chart-kit-preview-fallback">Loading chart playground</div></chart-kit-playground>`;
221+
previousNode.value = getPlaygroundHtml(id, previousNode.value, title);
154222
previousNode.children = [];
155223
tree.children.splice(index, 1);
156224
index -= 1;
@@ -223,6 +291,6 @@ export default function stripDuplicateTitle() {
223291
});
224292

225293
rewriteMarkdownLinks(tree, file);
226-
transformPreviewDirectives(tree);
294+
transformPreviewDirectives(tree, file);
227295
};
228296
}

apps/site/src/previews/ChartPlayground.tsx

Lines changed: 153 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ import {
2020
BarChart,
2121
ChartKitProvider,
2222
ContributionGraph,
23+
createChartPreset,
2324
DonutChart,
2425
LineChart,
2526
PieChart,
2627
ProgressChart,
28+
ProgressRing,
29+
StackedBarChart,
2730
type ChartKitThemeMode
2831
} from "react-native-chart-kit/v2";
32+
import { G, Line as SvgLine, Rect, Text as SvgText } from "react-native-svg";
2933

3034
import {
3135
acquisitionShare,
@@ -62,6 +66,34 @@ const getComponentName = (code: string) => {
6266
return declaration?.[1];
6367
};
6468

69+
const getStatementStyleLiveCode = (code: string) => {
70+
const lines = code.split("\n");
71+
const firstJsxLineIndex = lines.findIndex((line) =>
72+
/^\s*<[A-Z][\w.]*/.test(line)
73+
);
74+
75+
if (firstJsxLineIndex === -1) {
76+
return undefined;
77+
}
78+
79+
const setupCode = lines.slice(0, firstJsxLineIndex).join("\n").trim();
80+
const jsxCode = lines
81+
.slice(firstJsxLineIndex)
82+
.join("\n")
83+
.trim()
84+
.replace(/(\/>|<\/[A-Z][\w.]*>);/g, "$1");
85+
86+
return `function ChartKitLiveExample() {
87+
${setupCode ? `${setupCode}\n\n` : ""}return (
88+
<>
89+
${jsxCode}
90+
</>
91+
);
92+
}
93+
94+
render(<ChartKitLiveExample />);`;
95+
};
96+
6597
const prepareLiveCode = (code: string) => {
6698
const componentName = getComponentName(code);
6799
const runnableCode = code
@@ -80,6 +112,12 @@ const prepareLiveCode = (code: string) => {
80112
return `(() => {\n${runnableCode}\n\nrender(<${componentName} />);\n})();`;
81113
}
82114

115+
const statementStyleLiveCode = getStatementStyleLiveCode(runnableCode);
116+
117+
if (statementStyleLiveCode) {
118+
return `(() => {\n${statementStyleLiveCode}\n})();`;
119+
}
120+
83121
return `(() => {\nrender(${runnableCode});\n})();`;
84122
};
85123

@@ -98,6 +136,93 @@ const MAX_EDITOR_SIZE = 70;
98136
const clamp = (value: number, min: number, max: number) =>
99137
Math.min(max, Math.max(min, value));
100138

139+
const barPlaygroundData = signups.map((row) => ({
140+
...row,
141+
newCustomers: row.signups,
142+
organic: row.signups,
143+
paid: row.expansion,
144+
spend: row.signups,
145+
week: row.month
146+
}));
147+
148+
const linePlaygroundData = monthRevenue.map((row, index) => {
149+
const barRow = barPlaygroundData[index % barPlaygroundData.length];
150+
151+
return {
152+
...row,
153+
actual: row.revenue,
154+
attainment: row.retention,
155+
benchmark: row.forecast,
156+
date: row.month,
157+
expansion: barRow.expansion,
158+
newCustomers: barRow.newCustomers,
159+
organic: barRow.organic,
160+
paid: barRow.paid,
161+
portfolio: row.revenue,
162+
price: row.revenue,
163+
signups: barRow.signups,
164+
spend: barRow.spend,
165+
timestamp: index + 1,
166+
week: barRow.week
167+
};
168+
});
169+
170+
const revenueMixPlaygroundData = revenueMix.map((row) => ({
171+
...row,
172+
plan: row.label,
173+
revenue: row.value
174+
}));
175+
176+
const acquisitionSharePlaygroundData = acquisitionShare.map((row) => ({
177+
...row,
178+
share: row.value
179+
}));
180+
181+
const weeklySpend = Array.from({ length: 14 }, (_, index) => ({
182+
spend: 24 + Math.round(Math.sin(index / 2) * 8 + index * 2.4),
183+
week: `W${index + 1}`
184+
}));
185+
186+
const weeklyAcquisition = weeklySpend.map((row, index) => ({
187+
...row,
188+
organic: 32 + index * 4,
189+
paid: 18 + Math.round(Math.cos(index / 2) * 6 + index * 2)
190+
}));
191+
192+
const portfolioHistory = Array.from({ length: 120 }, (_, index) => {
193+
const portfolio = 84000 + index * 620 + Math.sin(index / 5) * 4200;
194+
const benchmark = 81000 + index * 520 + Math.cos(index / 7) * 3200;
195+
196+
return {
197+
benchmark,
198+
date: `Day ${index + 1}`,
199+
month: `Day ${index + 1}`,
200+
portfolio,
201+
price: portfolio,
202+
timestamp: index + 1
203+
};
204+
});
205+
206+
const largeData = portfolioHistory.map((row, index) => ({
207+
...row,
208+
price: 120 + index * 1.8 + Math.sin(index / 4) * 16
209+
}));
210+
211+
const retentionSegments = [
212+
{ accounts: 124, color: "#00163f", status: "Active" },
213+
{ accounts: 46, color: "#2f5f9f", status: "At risk" },
214+
{ accounts: 18, color: "#6f88aa", status: "Paused" }
215+
];
216+
217+
const previewDataById: Record<string, unknown[]> = {
218+
"bar-grouped": barPlaygroundData,
219+
"line-multi-series": linePlaygroundData,
220+
"line-selection": linePlaygroundData
221+
};
222+
223+
const getPreviewData = (id: string) =>
224+
previewDataById[id] ?? linePlaygroundData;
225+
101226
const writeClipboard = async (value: string) => {
102227
if (navigator.clipboard?.writeText) {
103228
await navigator.clipboard.writeText(value);
@@ -262,30 +387,53 @@ export const ChartPlayground = ({ code, id }: { code: string; id: string }) => {
262387
AreaChart,
263388
BarChart,
264389
ContributionGraph,
390+
ChartKitProvider,
265391
DonutChart,
392+
G,
266393
LineChart,
267394
PieChart,
268395
ProgressChart,
396+
ProgressRing,
269397
React,
398+
Rect,
399+
StackedBarChart,
400+
SvgText,
270401
Text,
271402
View,
272-
acquisitionShare,
403+
acquisitionShare: acquisitionSharePlaygroundData,
273404
clampChartWidth,
274405
contributionValues,
275-
data: monthRevenue,
406+
createChartPreset,
407+
data: getPreviewData(id),
408+
largeData,
276409
money,
277410
monthRevenue,
278411
percent,
412+
plans: revenueMixPlaygroundData,
279413
platformShare,
414+
portfolioHistory,
280415
previewWidth: clampChartWidth(width),
281416
profit,
282417
progressRings,
283-
revenueMix,
418+
revenueMix: revenueMixPlaygroundData,
419+
retentionSegments,
284420
signedMoney,
285421
signups,
286-
supportVolume
422+
setHeaderValue: () => undefined,
423+
setSelectedChannel: () => undefined,
424+
setSelectedDay: () => undefined,
425+
setViewport: () => undefined,
426+
supportVolume,
427+
SvgLine,
428+
Line: SvgLine,
429+
usageDays: contributionValues,
430+
useState: React.useState,
431+
values: contributionValues,
432+
viewport: { endIndex: 90, startIndex: 40 },
433+
weeklyAcquisition,
434+
weeklySpend
287435
}),
288-
[width]
436+
[id, width]
289437
);
290438

291439
const playgroundStyle = useMemo(

docs/charts/progress.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ description: Show circular progress values with configurable labels and accessib
77

88
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.
99

10+
## Concentric Rings
11+
1012
```tsx
11-
import { ProgressChart, ProgressRing } from "react-native-chart-kit/v2";
13+
import { ProgressChart } from "react-native-chart-kit/v2";
1214

1315
const data = [
1416
{ metric: "Move", progress: 0.72, color: "#f43f5e" },
@@ -26,6 +28,14 @@ const data = [
2628
preset="health"
2729
centerLabel={({ average }) => `${Math.round(average * 100)}%`}
2830
/>;
31+
```
32+
33+
::chart-preview{id="progress-rings"}
34+
35+
## Single Ring
36+
37+
```tsx
38+
import { ProgressRing } from "react-native-chart-kit/v2";
2939

3040
<ProgressRing
3141
value={0.64}
@@ -36,8 +46,6 @@ const data = [
3646
/>;
3747
```
3848

39-
::chart-preview{id="progress-rings"}
40-
4149
## Compatibility Shape
4250

4351
The legacy data object still works:

0 commit comments

Comments
 (0)