Vendor SEO chart kit and shell media
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 uixmat
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<p align="center">
|
||||||
|
<img alt="vercel-oss 2" src="https://github.com/user-attachments/assets/e6cd5d4c-7707-4271-96cb-10122d193048" />
|
||||||
|
</p>
|
||||||
|
</br>
|
||||||
|
<p align="center">
|
||||||
|
A collection of Open Source charts and utility components that you can customize and extend.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/bklit/bklit-ui"><img src="https://www.shieldcn.dev/github/stars/bklit/bklit-ui.svg?variant=secondary&size=xs&theme=zinc" alt="GitHub Stars" /></a>
|
||||||
|
<a href="https://github.com/bklit/bklit-ui/actions"><img src="https://www.shieldcn.dev/github/ci/bklit/bklit-ui.svg?variant=secondary&size=xs&theme=zinc" alt="CI" /></a>
|
||||||
|
<a href="https://discord.gg/75s4frfE8X"><img src="https://www.shieldcn.dev/discord/members/75s4frfE8X.svg?variant=secondary&size=xs&theme=zinc" alt="Discord Members" /></a>
|
||||||
|
<a href="https://x.com/uixmat"><img src="https://www.shieldcn.dev/x/follow/uixmat.svg?variant=branded&size=xs&theme=zinc" alt="X Follow" /></a>
|
||||||
|
<a href="https://vercel.com/oss"><img src="https://shieldcn.dev/badge/Vercel_OSS_Program_Member.svg?variant=branded&size=xs&theme=zinc&logo=vercel" alt="Vercel OSS Program Member" /></a>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Premium Sponsors
|
||||||
|
|
||||||
|
Bklit UI is supported by our premium sponsors.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<a href="https://openpanel.dev">
|
||||||
|
<img alt="OpenPanel" src="https://github.com/user-attachments/assets/279ef3ca-ed28-4a75-bac7-3fb90db4954d" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
**[OpenPanel](https://openpanel.dev)** — OpenPanel is an open source analytics platform that combines web analytics and product analytics in one privacy-first tool. Track pageviews, events, funnels, retention, and user journeys — all without cookies.
|
||||||
|
|
||||||
|
## Charts
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/area-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/area-chart.svg" alt="Area Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Area Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/area-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=area-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/bar-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/bar-chart.svg" alt="Bar Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Bar Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/bar-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=bar-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/candlestick-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/candlestick-chart.svg" alt="Candlestick Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Candlestick Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/candlestick-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=candlestick-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/choropleth-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/choropleth-chart.svg" alt="Choropleth Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Choropleth Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/choropleth-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=choropleth-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/composed-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/composed-chart.svg" alt="Composed Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Composed Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/composed-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=composed-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/funnel-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/funnel-chart.svg" alt="Funnel Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Funnel Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/funnel-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=funnel-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/gauge-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/gauge-chart.svg" alt="Gauge" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Gauge</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/gauge-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=gauge-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/line-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/line-chart.svg" alt="Line Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Line Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/line-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=line-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/live-line-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/live-line-chart.svg" alt="Live Line Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Live Line Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/live-line-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=live-line-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/pie-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/pie-chart.svg" alt="Pie Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Pie Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/pie-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=pie-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/radar-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/radar-chart.svg" alt="Radar Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Radar Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/radar-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=radar-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/ring-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/ring-chart.svg" alt="Ring Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Ring Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/ring-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=ring-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/scatter-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/scatter-chart.svg" alt="Scatter Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Scatter Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/scatter-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=scatter-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="33%">
|
||||||
|
<a href="https://ui.bklit.com/docs/components/sankey-chart">
|
||||||
|
<img src="https://github.com/bklit/bklit-ui/raw/main/apps/web/public/svgs/sankey-chart.svg" alt="Sankey Chart" width="280" />
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<strong>Sankey Chart</strong><br />
|
||||||
|
<a href="https://ui.bklit.com/docs/components/sankey-chart">Docs</a> · <a href="https://ui.bklit.com/studio?chart=sankey-chart">Studio</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Studio
|
||||||
|
|
||||||
|
Studio is Bklit's interactive chart playground. Pick a chart, tweak styling and animation controls in real time, then copy the generated React code or export a registry JSON for your project.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.youtube.com/watch?v=I3PLqHImYbE"><img src="https://img.youtube.com/vi/I3PLqHImYbE/maxresdefault.jpg" alt="Bklit Studio" width="720" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[Open Studio →](https://ui.bklit.com/studio)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Bklit UI is a shadcn registry. Initialize shadcn in your project, then add a chart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest init
|
||||||
|
npx shadcn@latest add @bklit/line-chart
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Installation](https://ui.bklit.com/docs/installation) for registry setup and all components.
|
||||||
|
|
||||||
|
## Repo activity
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
**Chart components** (`packages/ui`, shadcn registry) are [MIT licensed](./LICENSE).
|
||||||
|
|
||||||
|
**Bklit Studio** (`packages/studio`, hosted at [ui.bklit.com/studio](https://ui.bklit.com/studio))
|
||||||
|
is proprietary. See [LICENSE-STUDIO.md](./LICENSE-STUDIO.md). You may not reuse,
|
||||||
|
resell, or redistribute Studio without written permission.
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Bklit UI Vendor Snapshot
|
||||||
|
|
||||||
|
Source: https://github.com/bklit/bklit-ui
|
||||||
|
Commit: 86149e76b509b661aca26af95461b18952eda646
|
||||||
|
Snapshot date: 2026-06-28
|
||||||
|
|
||||||
|
Included:
|
||||||
|
|
||||||
|
- `LICENSE`
|
||||||
|
- `README.md`
|
||||||
|
- `packages/ui`
|
||||||
|
- `apps/web/public/svgs`
|
||||||
|
|
||||||
|
Excluded:
|
||||||
|
|
||||||
|
- `.git`
|
||||||
|
- `node_modules`
|
||||||
|
- `apps/studio`
|
||||||
|
- `packages/studio`
|
||||||
|
|
||||||
|
License notes:
|
||||||
|
|
||||||
|
- The root Bklit UI repository is MIT licensed.
|
||||||
|
- Bklit Studio has a separate restrictive license and is intentionally not vendored here.
|
||||||
|
- Treat this directory as a local source/reference snapshot. Do not import it into the runtime directly until the selected chart components and their dependencies are copied into the app layer intentionally.
|
||||||
|
|
||||||
|
Candidate SEO module uses:
|
||||||
|
|
||||||
|
- `funnel-chart`: pipeline/readiness from context review to market demand, SEO plan, rewrite, validation.
|
||||||
|
- `bar-chart`: top clusters, pages, intents, and demand buckets.
|
||||||
|
- `scatter-chart`: opportunity map by demand, relevance, confidence, and implementation effort.
|
||||||
|
- `sankey-chart`: context/intent to keyword/page/field flow for dev-mode explainability.
|
||||||
|
- `gauge-chart` or `ring-chart`: compact quality/readiness scores.
|
||||||
|
- `area-chart`, `line-chart`, `composed-chart`: historical demand, forecast, and validation trend screens after time-series data exists.
|
||||||
|
- `heatmap-chart`: keyword/page coverage matrix if the table becomes too dense.
|
||||||
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/area-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 995 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/bar-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1005 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/candlestick-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1000 KiB |
23
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/choropleth-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 675 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/composed-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1001 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/funnel-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1018 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/gauge-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1005 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/line-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 994 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/live-line-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1014 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/pie-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 988 KiB |
23
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/radar-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 500 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/ring-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 988 KiB |
23
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/sankey-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 499 KiB |
44
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/apps/web/public/svgs/scatter-chart.svg
vendored
Normal file
|
After Width: | Height: | Size: 1005 KiB |
17
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/typescript-config/react-library.json
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// biome-ignore lint/performance/noBarrelFile: ESLint config re-export
|
||||||
|
export { config as default } from "@bklitui/eslint-config/react-internal";
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"name": "@bklitui/ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"sideEffects": false,
|
||||||
|
"exports": {
|
||||||
|
"./charts": "./src/charts/index.ts",
|
||||||
|
"./lib/utils": "./src/lib/utils.ts",
|
||||||
|
"./components/shimmering-text": "./src/components/shimmering-text.tsx",
|
||||||
|
"./button": "./src/button.tsx",
|
||||||
|
"./card": "./src/card.tsx",
|
||||||
|
"./code": "./src/code.tsx",
|
||||||
|
"./chart-grid": "./src/chart-grid.tsx",
|
||||||
|
"./chart-marker": "./src/chart-marker.tsx",
|
||||||
|
"./line-chart": "./src/line-chart.tsx",
|
||||||
|
"./tooltip": "./src/tooltip.tsx",
|
||||||
|
"./*": "./src/*.tsx"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint . --max-warnings 0",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"test": "node scripts/run-tests.mjs",
|
||||||
|
"registry:build": "node scripts/verify-registry-files.mjs && node scripts/verify-registry-deps.mjs && node scripts/generate-v0-examples.mjs && node scripts/generate-block-registry.mjs && shadcn build --output ../../apps/web/public/r && cp registry.json ../../apps/web/public/r/registry.json",
|
||||||
|
"registry:verify": "node scripts/verify-registry-files.mjs && node scripts/verify-registry-deps.mjs && node scripts/verify-registry-sync.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@bklitui/eslint-config": "workspace:*",
|
||||||
|
"@bklitui/typescript-config": "workspace:*",
|
||||||
|
"@types/d3-array": "^3.2.1",
|
||||||
|
"@types/d3-geo": "^3.1.0",
|
||||||
|
"@types/d3-sankey": "^0.12.5",
|
||||||
|
"@types/d3-scale": "^4.0.9",
|
||||||
|
"@types/d3-shape": "^3.1.6",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@types/node": "^22.15.3",
|
||||||
|
"@types/react": "19.2.2",
|
||||||
|
"@types/react-dom": "19.2.2",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
|
"@types/topojson-specification": "^1.0.5",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"shadcn": "^2.5.0",
|
||||||
|
"tailwindcss": "^4.1.8",
|
||||||
|
"tsx": "^4.19.4",
|
||||||
|
"typescript": "5.9.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bklitui/icons": "workspace:*",
|
||||||
|
"@base-ui/react": "^1.0.0-alpha.8",
|
||||||
|
"@number-flow/react": "^0.5.4",
|
||||||
|
"@visx/brush": "3.12.0",
|
||||||
|
"@visx/curve": "4.0.1-alpha.0",
|
||||||
|
"@visx/event": "4.0.1-alpha.0",
|
||||||
|
"@visx/geo": "4.0.1-alpha.0",
|
||||||
|
"@visx/gradient": "4.0.1-alpha.0",
|
||||||
|
"@visx/grid": "4.0.1-alpha.0",
|
||||||
|
"@visx/group": "4.0.1-alpha.0",
|
||||||
|
"@visx/heatmap": "4.0.1-alpha.0",
|
||||||
|
"@visx/pattern": "4.0.1-alpha.0",
|
||||||
|
"@visx/responsive": "4.0.1-alpha.0",
|
||||||
|
"@visx/sankey": "4.0.1-alpha.0",
|
||||||
|
"@visx/scale": "4.0.1-alpha.0",
|
||||||
|
"@visx/shape": "4.0.1-alpha.0",
|
||||||
|
"@visx/zoom": "4.0.1-alpha.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"d3-array": "3.2.4",
|
||||||
|
"d3-geo": "3.1.0",
|
||||||
|
"d3-scale": "4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
|
"motion": "^12.27.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-use-measure": "^2.1.7",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"topojson-client": "3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
ChartStatFlow,
|
||||||
|
LinearGradient,
|
||||||
|
} from "@/components/charts";
|
||||||
|
import { curveCardinal } from "@visx/curve";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { revenueSeries, revenueStats } from "../data/revenue-series";
|
||||||
|
import {
|
||||||
|
StatCardChart,
|
||||||
|
statCardLabelClassName,
|
||||||
|
statCardValueClassName,
|
||||||
|
} from "./stat-card-chart";
|
||||||
|
import {
|
||||||
|
formatStatCardMonth,
|
||||||
|
StatCardHoverBridge,
|
||||||
|
type StatCardHoverState,
|
||||||
|
} from "./stat-card-hover-bridge";
|
||||||
|
import { TrendBadge } from "./trend-badge";
|
||||||
|
|
||||||
|
export function StatCardArea() {
|
||||||
|
const [hover, setHover] = useState<StatCardHoverState>({
|
||||||
|
value: null,
|
||||||
|
label: null,
|
||||||
|
trend: null,
|
||||||
|
});
|
||||||
|
const displayValue = hover.value ?? revenueStats.average;
|
||||||
|
const displayLabel = hover.label ?? "Avg";
|
||||||
|
const displayTrend = hover.trend ?? revenueStats.trend;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full gap-0 py-0">
|
||||||
|
<CardHeader className="px-4 py-3">
|
||||||
|
<CardTitle>Total Revenue</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<TrendBadge value={displayTrend} />
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-3 px-4 pt-2 pb-3">
|
||||||
|
<ChartStatFlow
|
||||||
|
formatOptions={{
|
||||||
|
currency: "USD",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
style: "currency",
|
||||||
|
}}
|
||||||
|
label={displayLabel}
|
||||||
|
labelClassName={statCardLabelClassName}
|
||||||
|
value={displayValue}
|
||||||
|
valueClassName={statCardValueClassName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCardChart size="md">
|
||||||
|
<AreaChart
|
||||||
|
aspectRatio="2.5 / 1"
|
||||||
|
className="w-full"
|
||||||
|
data={revenueSeries}
|
||||||
|
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||||
|
>
|
||||||
|
<StatCardHoverBridge
|
||||||
|
dataKey="value"
|
||||||
|
formatLabel={formatStatCardMonth}
|
||||||
|
onHoverChange={setHover}
|
||||||
|
/>
|
||||||
|
<LinearGradient
|
||||||
|
from="var(--chart-1)"
|
||||||
|
fromOpacity={0.45}
|
||||||
|
id="stat-card-area-fill"
|
||||||
|
to="var(--chart-1)"
|
||||||
|
toOpacity={0}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
curve={curveCardinal.tension(0.65)}
|
||||||
|
dataKey="value"
|
||||||
|
fill="url(#stat-card-area-fill)"
|
||||||
|
fillOpacity={1}
|
||||||
|
gradientToOpacity={0}
|
||||||
|
showHighlight
|
||||||
|
stroke="var(--chart-1)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</StatCardChart>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface StatCardHoverState {
|
||||||
|
value: number | null;
|
||||||
|
label: string | null;
|
||||||
|
trend: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statCardValueClassName =
|
||||||
|
"text-3xl font-semibold leading-none tracking-tight";
|
||||||
|
|
||||||
|
export const statCardLabelClassName = "mt-0 text-xs";
|
||||||
|
|
||||||
|
export const statCardChartHeights = {
|
||||||
|
sm: "[--stat-card-chart-h:96px]",
|
||||||
|
md: "[--stat-card-chart-h:190px]",
|
||||||
|
lg: "[--stat-card-chart-h:420px]",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Bleeds charts edge-to-edge inside stat card content padding. */
|
||||||
|
export function StatCardChart({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
size = "sm",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
size?: keyof typeof statCardChartHeights;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative -mx-4 -mb-3 overflow-hidden",
|
||||||
|
"[&_.relative.w-full]:aspect-auto! [&_.relative.w-full]:h-[var(--stat-card-chart-h)]!",
|
||||||
|
statCardChartHeights[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useChart } from "@/components/charts";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import type { StatCardHoverState } from "./stat-card-chart";
|
||||||
|
|
||||||
|
export type { StatCardHoverState } from "./stat-card-chart";
|
||||||
|
|
||||||
|
export function formatStatCardMonth(date: Date) {
|
||||||
|
return date.toLocaleDateString("en-US", { month: "short" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatStatCardWeekday(date: Date) {
|
||||||
|
return date.toLocaleDateString("en-US", { weekday: "long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePointDate(raw: unknown): Date | null {
|
||||||
|
if (raw instanceof Date) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
return new Date(raw);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePeriodTrend(
|
||||||
|
data: Record<string, unknown>[],
|
||||||
|
index: number,
|
||||||
|
dataKey: string
|
||||||
|
): number | null {
|
||||||
|
if (index <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = data[index]?.[dataKey];
|
||||||
|
const previous = data[index - 1]?.[dataKey];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof current !== "number" ||
|
||||||
|
typeof previous !== "number" ||
|
||||||
|
previous === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((current - previous) / previous) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Syncs hovered chart values, labels, and trend into stat card UI. */
|
||||||
|
export function StatCardHoverBridge({
|
||||||
|
dataKey,
|
||||||
|
dateKey = "date",
|
||||||
|
formatLabel,
|
||||||
|
onHoverChange,
|
||||||
|
}: {
|
||||||
|
dataKey: string;
|
||||||
|
dateKey?: string;
|
||||||
|
formatLabel: (date: Date) => string;
|
||||||
|
onHoverChange: (state: StatCardHoverState) => void;
|
||||||
|
}) {
|
||||||
|
const { data, tooltipData } = useChart();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tooltipData?.point) {
|
||||||
|
onHoverChange({ value: null, label: null, trend: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = tooltipData.point[dataKey];
|
||||||
|
const value = typeof raw === "number" ? raw : null;
|
||||||
|
const date = parsePointDate(tooltipData.point[dateKey]);
|
||||||
|
const label = date ? formatLabel(date) : null;
|
||||||
|
const trend = computePeriodTrend(data, tooltipData.index, dataKey);
|
||||||
|
|
||||||
|
onHoverChange({ value, label, trend });
|
||||||
|
}, [data, dataKey, dateKey, formatLabel, onHoverChange, tooltipData]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CentralIcon } from "@central-icons-react/all";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function TrendBadge({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const positive = value >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
positive &&
|
||||||
|
"border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
variant={positive ? "outline" : "destructive"}
|
||||||
|
>
|
||||||
|
<CentralIcon
|
||||||
|
className="size-3"
|
||||||
|
data-icon="inline-start"
|
||||||
|
fill="outlined"
|
||||||
|
join="round"
|
||||||
|
name={positive ? "IconArrowUp" : "IconArrowDown"}
|
||||||
|
radius="0"
|
||||||
|
stroke="1.5"
|
||||||
|
/>
|
||||||
|
{positive ? "+" : ""}
|
||||||
|
{value.toFixed(1)}%
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface StatCardHoverState {
|
||||||
|
value: number | null;
|
||||||
|
label: string | null;
|
||||||
|
trend: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statCardValueClassName =
|
||||||
|
"text-3xl font-semibold leading-none tracking-tight";
|
||||||
|
|
||||||
|
export const statCardLabelClassName = "mt-0 text-xs";
|
||||||
|
|
||||||
|
export const statCardChartHeights = {
|
||||||
|
sm: "[--stat-card-chart-h:96px]",
|
||||||
|
md: "[--stat-card-chart-h:190px]",
|
||||||
|
lg: "[--stat-card-chart-h:420px]",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Bleeds charts edge-to-edge inside stat card content padding. */
|
||||||
|
export function StatCardChart({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
size = "sm",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
size?: keyof typeof statCardChartHeights;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative -mx-4 -mb-3 overflow-hidden",
|
||||||
|
"[&_.relative.w-full]:aspect-auto! [&_.relative.w-full]:h-[var(--stat-card-chart-h)]!",
|
||||||
|
statCardChartHeights[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useChoropleth } from "@/components/charts";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { computeVisitorTrend, getVisitorValue } from "../data/visitors";
|
||||||
|
import type { StatCardHoverState } from "./stat-card-chart";
|
||||||
|
|
||||||
|
/** Syncs hovered choropleth feature into stat card NumberFlow and trend badge. */
|
||||||
|
export function StatCardChoroplethHoverBridge({
|
||||||
|
onHoverChange,
|
||||||
|
}: {
|
||||||
|
onHoverChange: (state: StatCardHoverState) => void;
|
||||||
|
}) {
|
||||||
|
const { tooltipData } = useChoropleth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tooltipData?.feature) {
|
||||||
|
onHoverChange({ value: null, label: null, trend: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feature = tooltipData.feature;
|
||||||
|
const label = (feature.properties?.name as string | undefined) ?? "Unknown";
|
||||||
|
const visitors = getVisitorValue(feature);
|
||||||
|
const value = visitors ?? 0;
|
||||||
|
const trend = visitors === undefined ? null : computeVisitorTrend(visitors);
|
||||||
|
|
||||||
|
onHoverChange({ value, label, trend });
|
||||||
|
}, [onHoverChange, tooltipData]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ChoroplethFeature } from "@/components/charts";
|
||||||
|
import {
|
||||||
|
ChartStatFlow,
|
||||||
|
ChoroplethChart,
|
||||||
|
ChoroplethFeatureComponent,
|
||||||
|
ChoroplethTooltip,
|
||||||
|
} from "@/components/charts";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useWorldDataStandalone } from "@/lib/use-world-data";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
getVisitorColor,
|
||||||
|
getVisitorValue,
|
||||||
|
visitorStats,
|
||||||
|
} from "../data/visitors";
|
||||||
|
import {
|
||||||
|
StatCardChart,
|
||||||
|
type StatCardHoverState,
|
||||||
|
statCardLabelClassName,
|
||||||
|
statCardValueClassName,
|
||||||
|
} from "./stat-card-chart";
|
||||||
|
import { StatCardChoroplethHoverBridge } from "./stat-card-choropleth-hover-bridge";
|
||||||
|
import { TrendBadge } from "./trend-badge";
|
||||||
|
|
||||||
|
export function StatCardChoropleth() {
|
||||||
|
const { worldData, isLoading } = useWorldDataStandalone();
|
||||||
|
const [hover, setHover] = useState<StatCardHoverState>({
|
||||||
|
value: null,
|
||||||
|
label: null,
|
||||||
|
trend: null,
|
||||||
|
});
|
||||||
|
const displayValue = hover.value ?? visitorStats.total;
|
||||||
|
const displayLabel = hover.label ?? "Total";
|
||||||
|
const displayTrend = hover.trend ?? visitorStats.trend;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="relative w-full gap-0 overflow-hidden py-0">
|
||||||
|
<CardHeader className="pointer-events-none absolute inset-x-0 top-0 z-10 grid auto-rows-min grid-cols-[1fr_auto] items-start gap-1 border-0 bg-gradient-to-b from-45% from-card to-transparent px-4 py-3 pb-10 shadow-none ring-0">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<CardTitle>Unique Visitors</CardTitle>
|
||||||
|
<ChartStatFlow
|
||||||
|
label={displayLabel}
|
||||||
|
labelClassName={statCardLabelClassName}
|
||||||
|
value={displayValue}
|
||||||
|
valueClassName={statCardValueClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CardAction>
|
||||||
|
<TrendBadge value={displayTrend} />
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{isLoading || !worldData ? (
|
||||||
|
<StatCardChart className="mx-0 mb-0 min-h-[420px]" size="lg">
|
||||||
|
<div className="flex h-full min-h-[420px] items-center justify-center text-muted-foreground text-xs">
|
||||||
|
Loading map…
|
||||||
|
</div>
|
||||||
|
</StatCardChart>
|
||||||
|
) : (
|
||||||
|
<StatCardChart className="mx-0 mb-0 min-h-[420px]" size="lg">
|
||||||
|
<ChoroplethChart
|
||||||
|
aspectRatio="2.5 / 1"
|
||||||
|
className="min-h-[420px] w-full"
|
||||||
|
data={worldData}
|
||||||
|
>
|
||||||
|
<StatCardChoroplethHoverBridge onHoverChange={setHover} />
|
||||||
|
<ChoroplethFeatureComponent
|
||||||
|
getFeatureColor={(feature: ChoroplethFeature) =>
|
||||||
|
getVisitorColor(feature)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChoroplethTooltip
|
||||||
|
getFeatureValue={getVisitorValue}
|
||||||
|
valueLabel="Visitors"
|
||||||
|
/>
|
||||||
|
</ChoroplethChart>
|
||||||
|
</StatCardChart>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CentralIcon } from "@central-icons-react/all";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function TrendBadge({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const positive = value >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
positive &&
|
||||||
|
"border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
variant={positive ? "outline" : "destructive"}
|
||||||
|
>
|
||||||
|
<CentralIcon
|
||||||
|
className="size-3"
|
||||||
|
data-icon="inline-start"
|
||||||
|
fill="outlined"
|
||||||
|
join="round"
|
||||||
|
name={positive ? "IconArrowUp" : "IconArrowDown"}
|
||||||
|
radius="0"
|
||||||
|
stroke="1.5"
|
||||||
|
/>
|
||||||
|
{positive ? "+" : ""}
|
||||||
|
{value.toFixed(1)}%
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { FeatureCollection, Geometry } from "geojson";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { feature } from "topojson-client";
|
||||||
|
import type { GeometryCollection, Topology } from "topojson-specification";
|
||||||
|
|
||||||
|
interface CountryProperties {
|
||||||
|
name: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorldTopology extends Topology {
|
||||||
|
objects: {
|
||||||
|
[key: string]: GeometryCollection<CountryProperties>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORLD_DATA_URL =
|
||||||
|
"https://raw.githubusercontent.com/subyfly/topojson/refs/heads/master/world-countries.json";
|
||||||
|
|
||||||
|
// Global cache to avoid refetching across component mounts
|
||||||
|
let globalWorldDataCache: FeatureCollection<
|
||||||
|
Geometry,
|
||||||
|
CountryProperties
|
||||||
|
> | null = null;
|
||||||
|
let globalFetchPromise: Promise<FeatureCollection<
|
||||||
|
Geometry,
|
||||||
|
CountryProperties
|
||||||
|
> | null> | null = null;
|
||||||
|
|
||||||
|
function fetchWorldData(): Promise<FeatureCollection<
|
||||||
|
Geometry,
|
||||||
|
CountryProperties
|
||||||
|
> | null> {
|
||||||
|
// Return cached data if available
|
||||||
|
if (globalWorldDataCache) {
|
||||||
|
return Promise.resolve(globalWorldDataCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return existing promise if fetch is in progress
|
||||||
|
if (globalFetchPromise) {
|
||||||
|
return globalFetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new fetch
|
||||||
|
globalFetchPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(WORLD_DATA_URL);
|
||||||
|
const topology = (await response.json()) as WorldTopology;
|
||||||
|
const objectKey = Object.keys(topology.objects)[0];
|
||||||
|
if (!objectKey) {
|
||||||
|
throw new Error("No objects found in topology");
|
||||||
|
}
|
||||||
|
const geoObject = topology.objects[objectKey];
|
||||||
|
if (!geoObject) {
|
||||||
|
throw new Error("Object not found in topology");
|
||||||
|
}
|
||||||
|
const geojson = feature(
|
||||||
|
topology,
|
||||||
|
geoObject
|
||||||
|
) as unknown as FeatureCollection<Geometry, CountryProperties>;
|
||||||
|
globalWorldDataCache = geojson;
|
||||||
|
return geojson;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch world data:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return globalFetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorldDataContextValue {
|
||||||
|
worldData: FeatureCollection<Geometry, CountryProperties> | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorldDataContext = createContext<WorldDataContextValue>({
|
||||||
|
worldData: null,
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function WorldDataProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [worldData, setWorldData] = useState<FeatureCollection<
|
||||||
|
Geometry,
|
||||||
|
CountryProperties
|
||||||
|
> | null>(globalWorldDataCache);
|
||||||
|
const [isLoading, setIsLoading] = useState(!globalWorldDataCache);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (globalWorldDataCache) {
|
||||||
|
setWorldData(globalWorldDataCache);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchWorldData().then((data) => {
|
||||||
|
setWorldData(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorldDataContext.Provider value={{ worldData, isLoading }}>
|
||||||
|
{children}
|
||||||
|
</WorldDataContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorldData() {
|
||||||
|
return useContext(WorldDataContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone hook for components that don't have the provider
|
||||||
|
export function useWorldDataStandalone() {
|
||||||
|
const [worldData, setWorldData] = useState<FeatureCollection<
|
||||||
|
Geometry,
|
||||||
|
CountryProperties
|
||||||
|
> | null>(globalWorldDataCache);
|
||||||
|
const [isLoading, setIsLoading] = useState(!globalWorldDataCache);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (globalWorldDataCache) {
|
||||||
|
setWorldData(globalWorldDataCache);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchWorldData().then((data) => {
|
||||||
|
setWorldData(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { worldData, isLoading };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface StatCardHoverState {
|
||||||
|
value: number | null;
|
||||||
|
label: string | null;
|
||||||
|
trend: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statCardValueClassName =
|
||||||
|
"text-3xl font-semibold leading-none tracking-tight";
|
||||||
|
|
||||||
|
export const statCardLabelClassName = "mt-0 text-xs";
|
||||||
|
|
||||||
|
export const statCardChartHeights = {
|
||||||
|
sm: "[--stat-card-chart-h:96px]",
|
||||||
|
md: "[--stat-card-chart-h:190px]",
|
||||||
|
lg: "[--stat-card-chart-h:420px]",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Bleeds charts edge-to-edge inside stat card content padding. */
|
||||||
|
export function StatCardChart({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
size = "sm",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
size?: keyof typeof statCardChartHeights;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative -mx-4 -mb-3 overflow-hidden",
|
||||||
|
"[&_.relative.w-full]:aspect-auto! [&_.relative.w-full]:h-[var(--stat-card-chart-h)]!",
|
||||||
|
statCardChartHeights[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useChart } from "@/components/charts";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import type { StatCardHoverState } from "./stat-card-chart";
|
||||||
|
|
||||||
|
export type { StatCardHoverState } from "./stat-card-chart";
|
||||||
|
|
||||||
|
export function formatStatCardMonth(date: Date) {
|
||||||
|
return date.toLocaleDateString("en-US", { month: "short" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatStatCardWeekday(date: Date) {
|
||||||
|
return date.toLocaleDateString("en-US", { weekday: "long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePointDate(raw: unknown): Date | null {
|
||||||
|
if (raw instanceof Date) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
return new Date(raw);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePeriodTrend(
|
||||||
|
data: Record<string, unknown>[],
|
||||||
|
index: number,
|
||||||
|
dataKey: string
|
||||||
|
): number | null {
|
||||||
|
if (index <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = data[index]?.[dataKey];
|
||||||
|
const previous = data[index - 1]?.[dataKey];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof current !== "number" ||
|
||||||
|
typeof previous !== "number" ||
|
||||||
|
previous === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((current - previous) / previous) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Syncs hovered chart values, labels, and trend into stat card UI. */
|
||||||
|
export function StatCardHoverBridge({
|
||||||
|
dataKey,
|
||||||
|
dateKey = "date",
|
||||||
|
formatLabel,
|
||||||
|
onHoverChange,
|
||||||
|
}: {
|
||||||
|
dataKey: string;
|
||||||
|
dateKey?: string;
|
||||||
|
formatLabel: (date: Date) => string;
|
||||||
|
onHoverChange: (state: StatCardHoverState) => void;
|
||||||
|
}) {
|
||||||
|
const { data, tooltipData } = useChart();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tooltipData?.point) {
|
||||||
|
onHoverChange({ value: null, label: null, trend: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = tooltipData.point[dataKey];
|
||||||
|
const value = typeof raw === "number" ? raw : null;
|
||||||
|
const date = parsePointDate(tooltipData.point[dateKey]);
|
||||||
|
const label = date ? formatLabel(date) : null;
|
||||||
|
const trend = computePeriodTrend(data, tooltipData.index, dataKey);
|
||||||
|
|
||||||
|
onHoverChange({ value, label, trend });
|
||||||
|
}, [data, dataKey, dateKey, formatLabel, onHoverChange, tooltipData]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ChartStatFlow, Line, LineChart } from "@/components/charts";
|
||||||
|
import { curveBasis } from "@visx/curve";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardAction,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { sessionsSeries, sessionsStats } from "../data/sessions-series";
|
||||||
|
import {
|
||||||
|
StatCardChart,
|
||||||
|
statCardLabelClassName,
|
||||||
|
statCardValueClassName,
|
||||||
|
} from "./stat-card-chart";
|
||||||
|
import {
|
||||||
|
formatStatCardWeekday,
|
||||||
|
StatCardHoverBridge,
|
||||||
|
type StatCardHoverState,
|
||||||
|
} from "./stat-card-hover-bridge";
|
||||||
|
import { TrendBadge } from "./trend-badge";
|
||||||
|
|
||||||
|
export function StatCardLine() {
|
||||||
|
const [hover, setHover] = useState<StatCardHoverState>({
|
||||||
|
value: null,
|
||||||
|
label: null,
|
||||||
|
trend: null,
|
||||||
|
});
|
||||||
|
const average = Math.round(sessionsStats.average);
|
||||||
|
const displayValue = hover.value === null ? average : Math.round(hover.value);
|
||||||
|
const displayLabel = hover.label ?? "Avg";
|
||||||
|
const displayTrend = hover.trend ?? sessionsStats.trend;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full gap-0 py-0">
|
||||||
|
<CardHeader className="px-4 py-3">
|
||||||
|
<CardTitle>Active Sessions</CardTitle>
|
||||||
|
<CardAction>
|
||||||
|
<TrendBadge value={displayTrend} />
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="px-4 pt-2 pb-3">
|
||||||
|
<StatCardChart size="md">
|
||||||
|
<div className="pointer-events-none absolute right-4 bottom-4 z-10 flex flex-col items-end text-right">
|
||||||
|
<ChartStatFlow
|
||||||
|
label={displayLabel}
|
||||||
|
labelClassName={statCardLabelClassName}
|
||||||
|
value={displayValue}
|
||||||
|
valueClassName={statCardValueClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LineChart
|
||||||
|
aspectRatio="2.5 / 1"
|
||||||
|
className="w-full"
|
||||||
|
data={sessionsSeries}
|
||||||
|
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||||
|
>
|
||||||
|
<StatCardHoverBridge
|
||||||
|
dataKey="value"
|
||||||
|
formatLabel={formatStatCardWeekday}
|
||||||
|
onHoverChange={setHover}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
curve={curveBasis}
|
||||||
|
dataKey="value"
|
||||||
|
showHighlight
|
||||||
|
stroke="var(--chart-3)"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</StatCardChart>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CentralIcon } from "@central-icons-react/all";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function TrendBadge({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const positive = value >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
positive &&
|
||||||
|
"border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
variant={positive ? "outline" : "destructive"}
|
||||||
|
>
|
||||||
|
<CentralIcon
|
||||||
|
className="size-3"
|
||||||
|
data-icon="inline-start"
|
||||||
|
fill="outlined"
|
||||||
|
join="round"
|
||||||
|
name={positive ? "IconArrowUp" : "IconArrowDown"}
|
||||||
|
radius="0"
|
||||||
|
stroke="1.5"
|
||||||
|
/>
|
||||||
|
{positive ? "+" : ""}
|
||||||
|
{value.toFixed(1)}%
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { AreaChart } from "./area-chart";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
30
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/area-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { AreaChart, Area, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { AreaChart, Area, Grid, XAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
import { curveNatural } from "@visx/curve";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), desktop: 186 },
|
||||||
|
{ date: new Date("2024-02-01"), desktop: 305 },
|
||||||
|
{ date: new Date("2024-03-01"), desktop: 237 },
|
||||||
|
{ date: new Date("2024-04-01"), desktop: 73 },
|
||||||
|
{ date: new Date("2024-05-01"), desktop: 209 },
|
||||||
|
{ date: new Date("2024-06-01"), desktop: 214 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<AreaChart data={chartData} animationDuration={1100}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Area dataKey="desktop" curve={curveNatural} strokeWidth={2.5} fillOpacity={0.4} />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</AreaChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { BarChart } from "./bar-chart";
|
||||||
|
export { Bar } from "./bar";
|
||||||
|
export { BarXAxis } from "./bar-x-axis";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
29
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/bar-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { BarChart, Bar, BarXAxis, Grid, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { BarChart, Bar, BarXAxis, Grid, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ month: "Jan", revenue: 12000, profit: 4500 },
|
||||||
|
{ month: "Feb", revenue: 15500, profit: 5200 },
|
||||||
|
{ month: "Mar", revenue: 11000, profit: 3800 },
|
||||||
|
{ month: "Apr", revenue: 18500, profit: 7100 },
|
||||||
|
{ month: "May", revenue: 16800, profit: 5400 },
|
||||||
|
{ month: "Jun", revenue: 21200, profit: 8800 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<BarChart data={data} xDataKey="month">
|
||||||
|
<Grid horizontal />
|
||||||
|
<Bar dataKey="revenue" fill="var(--chart-line-primary)" lineCap="round" />
|
||||||
|
<Bar dataKey="profit" fill="var(--chart-line-secondary)" lineCap="round" />
|
||||||
|
<BarXAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</BarChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { CandlestickChart } from "./candlestick-chart";
|
||||||
|
export { Candlestick } from "./candlestick";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { YAxis } from "./y-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
27
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/candlestick-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
const ohlcData = [
|
||||||
|
{ date: new Date("2024-01-02"), open: 100, high: 108, low: 98, close: 105 },
|
||||||
|
{ date: new Date("2024-01-03"), open: 105, high: 110, low: 102, close: 103 },
|
||||||
|
{ date: new Date("2024-01-04"), open: 103, high: 112, low: 101, close: 110 },
|
||||||
|
{ date: new Date("2024-01-05"), open: 110, high: 115, low: 107, close: 108 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<CandlestickChart data={ohlcData}>
|
||||||
|
<Grid horizontal vertical />
|
||||||
|
<Candlestick />
|
||||||
|
<XAxis />
|
||||||
|
<YAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</CandlestickChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "./choropleth";
|
||||||
23
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/choropleth-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ id: "US", name: "United States", value: 120 },
|
||||||
|
{ id: "CA", name: "Canada", value: 45 },
|
||||||
|
{ id: "GB", name: "United Kingdom", value: 62 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<ChoroplethChart data={features} aspectRatio="2 / 1">
|
||||||
|
<ChoroplethFeatureComponent />
|
||||||
|
<ChoroplethTooltip />
|
||||||
|
</ChoroplethChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { ComposedChart } from "./composed-chart";
|
||||||
|
export { SeriesBar } from "./series-bar";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
30
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/composed-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
import { curveNatural } from "@visx/curve";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), revenue: 4200, runRate: 3800 },
|
||||||
|
{ date: new Date("2024-02-01"), revenue: 5100, runRate: 4600 },
|
||||||
|
{ date: new Date("2024-03-01"), revenue: 4800, runRate: 5200 },
|
||||||
|
{ date: new Date("2024-04-01"), revenue: 5500, runRate: 5000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<ComposedChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<SeriesBar dataKey="revenue" fill="var(--chart-1)" />
|
||||||
|
<Area dataKey="runRate" curve={curveNatural} fill="var(--chart-4)" fillOpacity={0.35} />
|
||||||
|
<Line dataKey="runRate" curve={curveNatural} stroke="var(--chart-2)" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</ComposedChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { FunnelChart } from "./funnel-chart";
|
||||||
21
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/funnel-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { FunnelChart } from "@bklitui/ui/charts"
|
||||||
|
import { FunnelChart } from "@/components/charts"
|
||||||
|
|
||||||
|
const funnelData = [
|
||||||
|
{ label: "Visitors", value: 12000 },
|
||||||
|
{ label: "Signups", value: 4800 },
|
||||||
|
{ label: "Activated", value: 2100 },
|
||||||
|
{ label: "Paid", value: 840 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<FunnelChart data={funnelData} aspectRatio="2 / 1" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { Gauge } from "./gauge";
|
||||||
22
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/gauge-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { Gauge } from "@bklitui/ui/charts"
|
||||||
|
import { Gauge } from "@/components/charts"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<Gauge
|
||||||
|
value={72}
|
||||||
|
centerValue={72}
|
||||||
|
totalNotches={40}
|
||||||
|
defaultLabel="Score"
|
||||||
|
formatOptions={{ style: "percent" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export {
|
||||||
|
HeatmapCells,
|
||||||
|
HeatmapChart,
|
||||||
|
HeatmapInteractionBoundary,
|
||||||
|
HeatmapInteractionProvider,
|
||||||
|
HeatmapLegend,
|
||||||
|
HeatmapTooltip,
|
||||||
|
HeatmapXAxis,
|
||||||
|
HeatmapYAxis,
|
||||||
|
} from "./heatmap";
|
||||||
53
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/heatmap-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis } from "@bklitui/ui/charts"
|
||||||
|
import { HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis } from "@/components/charts"
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
bin: 0,
|
||||||
|
bins: [
|
||||||
|
{ bin: 0, count: 2, date: new Date(2024, 0, 1) },
|
||||||
|
{ bin: 1, count: 0, date: new Date(2024, 0, 2) },
|
||||||
|
{ bin: 2, count: 3, date: new Date(2024, 0, 3) },
|
||||||
|
{ bin: 3, count: 1, date: new Date(2024, 0, 4) },
|
||||||
|
{ bin: 4, count: 4, date: new Date(2024, 0, 5) },
|
||||||
|
{ bin: 5, count: 0, date: new Date(2024, 0, 6) },
|
||||||
|
{ bin: 6, count: 1, date: new Date(2024, 0, 7) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bin: 1,
|
||||||
|
bins: [
|
||||||
|
{ bin: 0, count: 1, date: new Date(2024, 0, 8) },
|
||||||
|
{ bin: 1, count: 2, date: new Date(2024, 0, 9) },
|
||||||
|
{ bin: 2, count: 0, date: new Date(2024, 0, 10) },
|
||||||
|
{ bin: 3, count: 3, date: new Date(2024, 0, 11) },
|
||||||
|
{ bin: 4, count: 2, date: new Date(2024, 0, 12) },
|
||||||
|
{ bin: 5, count: 1, date: new Date(2024, 0, 13) },
|
||||||
|
{ bin: 6, count: 0, date: new Date(2024, 0, 14) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<HeatmapInteractionProvider>
|
||||||
|
<HeatmapInteractionBoundary>
|
||||||
|
<div className="flex w-full flex-col items-stretch gap-3">
|
||||||
|
<HeatmapChart className="w-full" data={data} layout="fluid">
|
||||||
|
<HeatmapCells />
|
||||||
|
<HeatmapXAxis />
|
||||||
|
<HeatmapYAxis />
|
||||||
|
<HeatmapTooltip />
|
||||||
|
</HeatmapChart>
|
||||||
|
<HeatmapLegend />
|
||||||
|
</div>
|
||||||
|
</HeatmapInteractionBoundary>
|
||||||
|
</HeatmapInteractionProvider>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { LineChart } from "./line-chart";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
30
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/line-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
import { curveNatural } from "@visx/curve";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), users: 1200 },
|
||||||
|
{ date: new Date("2024-02-01"), users: 1350 },
|
||||||
|
{ date: new Date("2024-03-01"), users: 1100 },
|
||||||
|
{ date: new Date("2024-04-01"), users: 1450 },
|
||||||
|
{ date: new Date("2024-05-01"), users: 1380 },
|
||||||
|
{ date: new Date("2024-06-01"), users: 1520 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Line dataKey="users" curve={curveNatural} stroke="var(--chart-line-primary)" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</LineChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { LiveLineChart } from "./live-line-chart";
|
||||||
|
export { LiveLine } from "./live-line";
|
||||||
|
export { LiveXAxis } from "./live-x-axis";
|
||||||
|
export { LiveYAxis } from "./live-y-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
32
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/live-line-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
import { curveNatural } from "@visx/curve";
|
||||||
|
|
||||||
|
const initialData = Array.from({ length: 24 }, (_, i) => ({
|
||||||
|
time: Date.now() - (23 - i) * 60_000,
|
||||||
|
value: 50 + Math.sin(i / 3) * 20,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<LiveLineChart
|
||||||
|
data={initialData}
|
||||||
|
interval={1000}
|
||||||
|
maxPoints={24}
|
||||||
|
xDataKey="time"
|
||||||
|
yDataKey="value"
|
||||||
|
>
|
||||||
|
<LiveLine curve={curveNatural} stroke="var(--chart-line-primary)" />
|
||||||
|
<LiveXAxis />
|
||||||
|
<LiveYAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</LiveLineChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { PieChart } from "./pie-chart";
|
||||||
|
export { PieSlice } from "./pie-slice";
|
||||||
|
export { PieCenter } from "./pie-center";
|
||||||
26
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/pie-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { PieChart, PieSlice, PieCenter } from "@bklitui/ui/charts"
|
||||||
|
import { PieChart, PieSlice, PieCenter } from "@/components/charts"
|
||||||
|
|
||||||
|
const pieData = [
|
||||||
|
{ label: "Direct", value: 320 },
|
||||||
|
{ label: "Organic", value: 280 },
|
||||||
|
{ label: "Referral", value: 190 },
|
||||||
|
{ label: "Social", value: 140 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<PieChart data={pieData} size={280}>
|
||||||
|
{pieData.map((item, i) => (
|
||||||
|
<PieSlice index={i} key={item.label} />
|
||||||
|
))}
|
||||||
|
<PieCenter defaultLabel="Traffic" />
|
||||||
|
</PieChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { LineChart } from "./line-chart";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
|
export { ProfitLossLine } from "./profit-loss-line";
|
||||||
|
export { ProfitLossLegend } from "./profit-loss-legend";
|
||||||
|
export { ProfitLossLegendHoverProvider } from "./profit-loss-legend-hover";
|
||||||
|
export {
|
||||||
|
profitLossColor,
|
||||||
|
resolveProfitLossTooltipLabel,
|
||||||
|
} from "./profit-loss-line";
|
||||||
72
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/profit-loss-line.tsx
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import from "@bklitui/ui/charts"
|
||||||
|
import {
|
||||||
|
ChartTooltip,
|
||||||
|
Grid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ProfitLossLegend,
|
||||||
|
ProfitLossLegendHoverProvider,
|
||||||
|
ProfitLossLine,
|
||||||
|
profitLossColor,
|
||||||
|
resolveProfitLossTooltipLabel,
|
||||||
|
XAxis,
|
||||||
|
} from "@/components/charts"
|
||||||
|
import { curveLinear } from "@visx/curve";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), pnl: 420 },
|
||||||
|
{ date: new Date("2024-01-05"), pnl: 180 },
|
||||||
|
{ date: new Date("2024-01-10"), pnl: -240 },
|
||||||
|
{ date: new Date("2024-01-15"), pnl: -90 },
|
||||||
|
{ date: new Date("2024-01-20"), pnl: 310 },
|
||||||
|
{ date: new Date("2024-01-25"), pnl: 520 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
const [legendHoveredIndex, setLegendHoveredIndex] = useState<number | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="flex w-full max-w-3xl flex-col gap-2">
|
||||||
|
<ProfitLossLegend
|
||||||
|
align="center"
|
||||||
|
hoveredIndex={legendHoveredIndex}
|
||||||
|
onHoverChange={setLegendHoveredIndex}
|
||||||
|
/>
|
||||||
|
<LineChart data={chartData}>
|
||||||
|
<Grid highlightRowValues={[0]} horizontal />
|
||||||
|
<Line
|
||||||
|
curve={curveLinear}
|
||||||
|
dataKey="pnl"
|
||||||
|
fadeEdges={false}
|
||||||
|
showHighlight={false}
|
||||||
|
stroke="transparent"
|
||||||
|
strokeWidth={0}
|
||||||
|
/>
|
||||||
|
<ProfitLossLegendHoverProvider hoveredIndex={legendHoveredIndex}>
|
||||||
|
<ProfitLossLine dataKey="pnl" />
|
||||||
|
</ProfitLossLegendHoverProvider>
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip
|
||||||
|
indicatorColor={(point) => profitLossColor((point.pnl as number) ?? 0)}
|
||||||
|
rows={(point) => {
|
||||||
|
const value = (point.pnl as number) ?? 0;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
color: profitLossColor(value),
|
||||||
|
label: resolveProfitLossTooltipLabel(""),
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { RadarChart } from "./radar-chart";
|
||||||
|
export { RadarGrid } from "./radar-grid";
|
||||||
|
export { RadarAxis } from "./radar-axis";
|
||||||
|
export { RadarLabels } from "./radar-labels";
|
||||||
|
export { RadarArea } from "./radar-area";
|
||||||
32
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/radar-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea } from "@bklitui/ui/charts"
|
||||||
|
import { RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea } from "@/components/charts"
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{ key: "speed", label: "Speed" },
|
||||||
|
{ key: "reliability", label: "Reliability" },
|
||||||
|
{ key: "comfort", label: "Comfort" },
|
||||||
|
{ key: "safety", label: "Safety" },
|
||||||
|
{ key: "efficiency", label: "Efficiency" },
|
||||||
|
];
|
||||||
|
const data = [
|
||||||
|
{ id: "a", speed: 80, reliability: 70, comfort: 60, safety: 90, efficiency: 75 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<RadarChart data={data} metrics={metrics} size={320}>
|
||||||
|
<RadarGrid />
|
||||||
|
<RadarAxis />
|
||||||
|
<RadarLabels fontSize={10} offset={16} />
|
||||||
|
{data.map((row, i) => (
|
||||||
|
<RadarArea key={row.id} index={i} fill="var(--chart-line-primary)" fillOpacity={0.35} />
|
||||||
|
))}
|
||||||
|
</RadarChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart-index.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { RingChart } from "./ring-chart";
|
||||||
|
export { Ring } from "./ring";
|
||||||
|
export { RingCenter } from "./ring-center";
|
||||||
26
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/ring-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { RingChart, Ring, RingCenter } from "@bklitui/ui/charts"
|
||||||
|
import { RingChart, Ring, RingCenter } from "@/components/charts"
|
||||||
|
|
||||||
|
const ringData = [
|
||||||
|
{ label: "Email", value: 42 },
|
||||||
|
{ label: "Social", value: 28 },
|
||||||
|
{ label: "Direct", value: 18 },
|
||||||
|
{ label: "Other", value: 12 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<RingChart data={ringData} size={280} strokeWidth={14}>
|
||||||
|
{ringData.map((item, i) => (
|
||||||
|
<Ring index={i} key={item.label} />
|
||||||
|
))}
|
||||||
|
<RingCenter defaultLabel="Channels" />
|
||||||
|
</RingChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "./sankey";
|
||||||
34
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/sankey-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
nodes: [
|
||||||
|
{ name: "Ads" },
|
||||||
|
{ name: "Organic" },
|
||||||
|
{ name: "Landing" },
|
||||||
|
{ name: "Product" },
|
||||||
|
{ name: "Checkout" },
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ source: 0, target: 2, value: 40 },
|
||||||
|
{ source: 1, target: 2, value: 30 },
|
||||||
|
{ source: 2, target: 3, value: 50 },
|
||||||
|
{ source: 3, target: 4, value: 35 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<SankeyChart data={data} aspectRatio="16 / 9">
|
||||||
|
<SankeyLink />
|
||||||
|
<SankeyNode />
|
||||||
|
<SankeyTooltip />
|
||||||
|
</SankeyChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install
|
||||||
|
export { ScatterChart } from "./scatter-chart";
|
||||||
|
export { Scatter } from "./scatter";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";
|
||||||
29
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/scatter-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { ScatterChart, Scatter, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts"
|
||||||
|
import { ScatterChart, Scatter, Grid, XAxis, ChartTooltip } from "@/components/charts"
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), sessions: 420, conversions: 28 },
|
||||||
|
{ date: new Date("2024-02-01"), sessions: 510, conversions: 34 },
|
||||||
|
{ date: new Date("2024-03-01"), sessions: 390, conversions: 22 },
|
||||||
|
{ date: new Date("2024-04-01"), sessions: 580, conversions: 41 },
|
||||||
|
{ date: new Date("2024-05-01"), sessions: 620, conversions: 38 },
|
||||||
|
{ date: new Date("2024-06-01"), sessions: 710, conversions: 52 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<ScatterChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Scatter dataKey="sessions" />
|
||||||
|
<Scatter dataKey="conversions" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</ScatterChart>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */
|
||||||
|
export { AreaChart } from "./area-chart";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";
|
||||||
|
export { useChart } from "./chart-context";
|
||||||
|
export { LinearGradient } from "@visx/gradient";
|
||||||
13
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-area-01.tsx
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { StatCardArea } from "@/components/stat-card-area";
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<StatCardArea />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */
|
||||||
|
export { type ChoroplethFeature, ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip, useChoropleth } from "./choropleth";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { StatCardChoropleth } from "@/components/stat-card-choropleth";
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<StatCardChoropleth />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */
|
||||||
|
export { LineChart } from "./line-chart";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";
|
||||||
|
export { useChart } from "./chart-context";
|
||||||
13
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/registry/examples/stat-card-line-01.tsx
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { StatCardLine } from "@/components/stat-card-line";
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<StatCardLine />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
328
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-block-registry.mjs
vendored
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
/**
|
||||||
|
* Syncs stat-card blocks from apps/web/blocks into registry sources and registry.json
|
||||||
|
* (shadcn add + Open in v0). Run from packages/ui after generate-v0-examples.mjs.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
cpSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const uiRoot = join(__dirname, "..");
|
||||||
|
const webBlocksRoot = join(uiRoot, "../../apps/web/blocks");
|
||||||
|
const registryBlocksRoot = join(uiRoot, "registry/blocks");
|
||||||
|
const examplesDir = join(uiRoot, "registry/examples");
|
||||||
|
const registryPath = join(uiRoot, "registry.json");
|
||||||
|
|
||||||
|
const TREND_BADGE_REGISTRY = `"use client";
|
||||||
|
|
||||||
|
import { CentralIcon } from "@central-icons-react/all";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function TrendBadge({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const positive = value >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={cn(
|
||||||
|
positive &&
|
||||||
|
"border-emerald-500/20 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
variant={positive ? "outline" : "destructive"}
|
||||||
|
>
|
||||||
|
<CentralIcon
|
||||||
|
className="size-3"
|
||||||
|
data-icon="inline-start"
|
||||||
|
fill="outlined"
|
||||||
|
join="round"
|
||||||
|
name={positive ? "IconArrowUp" : "IconArrowDown"}
|
||||||
|
radius="0"
|
||||||
|
stroke="1.5"
|
||||||
|
/>
|
||||||
|
{positive ? "+" : ""}
|
||||||
|
{value.toFixed(1)}%
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BLOCK_CHART_INDEX = {
|
||||||
|
"stat-card-area-01": `export { AreaChart } from "./area-chart";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";
|
||||||
|
export { useChart } from "./chart-context";
|
||||||
|
export { LinearGradient } from "@visx/gradient";`,
|
||||||
|
"stat-card-line-01": `export { LineChart } from "./line-chart";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";
|
||||||
|
export { useChart } from "./chart-context";`,
|
||||||
|
"stat-card-choropleth-01": `export { type ChoroplethFeature, ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip, useChoropleth } from "./choropleth";
|
||||||
|
export { ChartStatFlow } from "./chart-stat-flow";`,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {Record<string, { title: string, description: string, previewMaxWidth: string, registryDependencies: string[], dependencies: string[], files: string[], extraFiles?: { path: string, target: string, content?: string }[] }>} */
|
||||||
|
const BLOCKS = {
|
||||||
|
"stat-card-area-01": {
|
||||||
|
title: "Stat Card Area",
|
||||||
|
description:
|
||||||
|
"Revenue stat card with gradient area sparkline, NumberFlow, and trend badge",
|
||||||
|
previewMaxWidth: "max-w-md",
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/area-chart",
|
||||||
|
"@bklit/chart-stat-flow",
|
||||||
|
"card",
|
||||||
|
"badge",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@visx/gradient@4.0.1-alpha.0",
|
||||||
|
"@number-flow/react",
|
||||||
|
"@central-icons-react/all",
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
"components/stat-card-area.tsx",
|
||||||
|
"components/stat-card-chart.tsx",
|
||||||
|
"components/stat-card-hover-bridge.tsx",
|
||||||
|
"components/trend-badge.tsx",
|
||||||
|
"data/revenue-series.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stat-card-line-01": {
|
||||||
|
title: "Stat Card Line",
|
||||||
|
description:
|
||||||
|
"Sessions stat card with line sparkline, overlaid NumberFlow, and trend badge",
|
||||||
|
previewMaxWidth: "max-w-md",
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/line-chart",
|
||||||
|
"@bklit/chart-stat-flow",
|
||||||
|
"card",
|
||||||
|
"badge",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@number-flow/react",
|
||||||
|
"@central-icons-react/all",
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
"components/stat-card-line.tsx",
|
||||||
|
"components/stat-card-chart.tsx",
|
||||||
|
"components/stat-card-hover-bridge.tsx",
|
||||||
|
"components/trend-badge.tsx",
|
||||||
|
"data/sessions-series.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"stat-card-choropleth-01": {
|
||||||
|
title: "Stat Card Choropleth",
|
||||||
|
description:
|
||||||
|
"Visitor map stat card with choropleth sparkline, NumberFlow, and trend badge",
|
||||||
|
previewMaxWidth: "max-w-3xl",
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/choropleth-chart",
|
||||||
|
"@bklit/chart-stat-flow",
|
||||||
|
"card",
|
||||||
|
"badge",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@number-flow/react",
|
||||||
|
"@types/geojson",
|
||||||
|
"@types/topojson-specification",
|
||||||
|
"@central-icons-react/all",
|
||||||
|
"topojson-client",
|
||||||
|
],
|
||||||
|
files: [
|
||||||
|
"components/stat-card-choropleth.tsx",
|
||||||
|
"components/stat-card-choropleth-hover-bridge.tsx",
|
||||||
|
"components/stat-card-chart.tsx",
|
||||||
|
"components/trend-badge.tsx",
|
||||||
|
"data/visitors.ts",
|
||||||
|
"lib/use-world-data.tsx",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENT_EXPORTS = {
|
||||||
|
"stat-card-area-01": "StatCardArea",
|
||||||
|
"stat-card-line-01": "StatCardLine",
|
||||||
|
"stat-card-choropleth-01": "StatCardChoropleth",
|
||||||
|
};
|
||||||
|
|
||||||
|
function transformForRegistry(content, filePath) {
|
||||||
|
if (filePath.endsWith("trend-badge.tsx")) {
|
||||||
|
return TREND_BADGE_REGISTRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
.replaceAll("@bklitui/ui/charts", "@/components/charts")
|
||||||
|
.replaceAll("@/components/docs/use-world-data", "@/lib/use-world-data");
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyBlockFile(blockId, relativePath) {
|
||||||
|
const sourcePath = join(webBlocksRoot, blockId, "files", relativePath);
|
||||||
|
const targetPath = join(registryBlocksRoot, blockId, relativePath);
|
||||||
|
|
||||||
|
if (relativePath === "lib/use-world-data.tsx") {
|
||||||
|
const sharedSource = join(
|
||||||
|
uiRoot,
|
||||||
|
"../../apps/web/components/docs/use-world-data.tsx"
|
||||||
|
);
|
||||||
|
mkdirSync(dirname(targetPath), { recursive: true });
|
||||||
|
cpSync(sharedSource, targetPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(sourcePath)) {
|
||||||
|
throw new Error(`Block source not found: ${sourcePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(dirname(targetPath), { recursive: true });
|
||||||
|
const content = transformForRegistry(
|
||||||
|
readFileSync(sourcePath, "utf8"),
|
||||||
|
relativePath
|
||||||
|
);
|
||||||
|
writeFileSync(targetPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockRegistryFiles(blockId, relativePaths) {
|
||||||
|
return relativePaths.map((relativePath) => ({
|
||||||
|
path: `registry/blocks/${blockId}/${relativePath}`,
|
||||||
|
type: relativePath.endsWith(".ts") ? "registry:lib" : "registry:component",
|
||||||
|
target: relativePath,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockChartIndexFile(blockId) {
|
||||||
|
if (!BLOCK_CHART_INDEX[blockId]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: `registry/examples/${blockId}-index.ts`,
|
||||||
|
type: "registry:lib",
|
||||||
|
target: "components/charts/index.ts",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartStatFlowItem = {
|
||||||
|
name: "chart-stat-flow",
|
||||||
|
type: "registry:component",
|
||||||
|
title: "Chart Stat Flow",
|
||||||
|
description: "Animated stat value and label for chart overlays",
|
||||||
|
registryDependencies: ["@bklit/utils"],
|
||||||
|
dependencies: ["@number-flow/react", "motion"],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "src/charts/chart-stat-flow.tsx",
|
||||||
|
type: "registry:component",
|
||||||
|
target: "components/charts/chart-stat-flow.tsx",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdirSync(examplesDir, { recursive: true });
|
||||||
|
mkdirSync(registryBlocksRoot, { recursive: true });
|
||||||
|
|
||||||
|
for (const [blockId, spec] of Object.entries(BLOCKS)) {
|
||||||
|
for (const relativePath of spec.files) {
|
||||||
|
copyBlockFile(blockId, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportName = COMPONENT_EXPORTS[blockId];
|
||||||
|
const mainComponentPath = spec.files.find((file) =>
|
||||||
|
file.startsWith("components/stat-card-")
|
||||||
|
);
|
||||||
|
const importPath = `@/components/${mainComponentPath
|
||||||
|
?.replace("components/", "")
|
||||||
|
.replace(".tsx", "")}`;
|
||||||
|
|
||||||
|
const exampleContent = `"use client";
|
||||||
|
|
||||||
|
import { ${exportName} } from "${importPath}";
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full ${spec.previewMaxWidth}">
|
||||||
|
<${exportName} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(join(examplesDir, `${blockId}.tsx`), exampleContent);
|
||||||
|
|
||||||
|
const chartIndex = BLOCK_CHART_INDEX[blockId];
|
||||||
|
if (chartIndex) {
|
||||||
|
writeFileSync(
|
||||||
|
join(examplesDir, `${blockId}-index.ts`),
|
||||||
|
`/** biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install */\n${chartIndex}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = JSON.parse(readFileSync(registryPath, "utf8"));
|
||||||
|
|
||||||
|
registry.items = registry.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.name !== "chart-stat-flow" && !item.name.startsWith("stat-card-")
|
||||||
|
);
|
||||||
|
|
||||||
|
registry.items.push(chartStatFlowItem);
|
||||||
|
|
||||||
|
for (const [blockId, spec] of Object.entries(BLOCKS)) {
|
||||||
|
const blockFiles = blockRegistryFiles(blockId, spec.files);
|
||||||
|
const chartIndexFile = blockChartIndexFile(blockId);
|
||||||
|
if (chartIndexFile) {
|
||||||
|
blockFiles.push(chartIndexFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.items.push({
|
||||||
|
name: blockId,
|
||||||
|
type: "registry:block",
|
||||||
|
title: spec.title,
|
||||||
|
description: spec.description,
|
||||||
|
registryDependencies: spec.registryDependencies,
|
||||||
|
dependencies: spec.dependencies,
|
||||||
|
files: blockFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const exampleFiles = [
|
||||||
|
{
|
||||||
|
path: `registry/examples/${blockId}.tsx`,
|
||||||
|
type: "registry:page",
|
||||||
|
target: "app/page.tsx",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (chartIndexFile) {
|
||||||
|
exampleFiles.push(chartIndexFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.items.push({
|
||||||
|
name: `${blockId}-example`,
|
||||||
|
type: "registry:example",
|
||||||
|
title: `${spec.title} Example`,
|
||||||
|
description: `${spec.description} — demo for Open in v0`,
|
||||||
|
registryDependencies: [`@bklit/${blockId}`],
|
||||||
|
dependencies: spec.dependencies,
|
||||||
|
files: exampleFiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
||||||
|
console.log(
|
||||||
|
`Synced ${Object.keys(BLOCKS).length} stat-card blocks into registry (${Object.keys(BLOCKS).length * 2 + 1} items).`
|
||||||
|
);
|
||||||
520
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/generate-v0-examples.mjs
vendored
Normal file
|
|
@ -0,0 +1,520 @@
|
||||||
|
/**
|
||||||
|
* Generates registry example entries for Open in v0 (registry:example + registry:page).
|
||||||
|
* Run from packages/ui: node scripts/generate-v0-examples.mjs
|
||||||
|
*/
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = join(__dirname, "..");
|
||||||
|
const examplesDir = join(root, "registry/examples");
|
||||||
|
|
||||||
|
/** Per-chart barrel for installed @/components/charts/* paths in v0. */
|
||||||
|
const CHART_INDEX_EXPORTS = {
|
||||||
|
"area-chart": `export { AreaChart } from "./area-chart";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"bar-chart": `export { BarChart } from "./bar-chart";
|
||||||
|
export { Bar } from "./bar";
|
||||||
|
export { BarXAxis } from "./bar-x-axis";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"line-chart": `export { LineChart } from "./line-chart";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"scatter-chart": `export { ScatterChart } from "./scatter-chart";
|
||||||
|
export { Scatter } from "./scatter";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"pie-chart": `export { PieChart } from "./pie-chart";
|
||||||
|
export { PieSlice } from "./pie-slice";
|
||||||
|
export { PieCenter } from "./pie-center";`,
|
||||||
|
"gauge-chart": `export { Gauge } from "./gauge";`,
|
||||||
|
"heatmap-chart": `export {
|
||||||
|
HeatmapCells,
|
||||||
|
HeatmapChart,
|
||||||
|
HeatmapInteractionBoundary,
|
||||||
|
HeatmapInteractionProvider,
|
||||||
|
HeatmapLegend,
|
||||||
|
HeatmapTooltip,
|
||||||
|
HeatmapXAxis,
|
||||||
|
HeatmapYAxis,
|
||||||
|
} from "./heatmap";`,
|
||||||
|
"ring-chart": `export { RingChart } from "./ring-chart";
|
||||||
|
export { Ring } from "./ring";
|
||||||
|
export { RingCenter } from "./ring-center";`,
|
||||||
|
"radar-chart": `export { RadarChart } from "./radar-chart";
|
||||||
|
export { RadarGrid } from "./radar-grid";
|
||||||
|
export { RadarAxis } from "./radar-axis";
|
||||||
|
export { RadarLabels } from "./radar-labels";
|
||||||
|
export { RadarArea } from "./radar-area";`,
|
||||||
|
"composed-chart": `export { ComposedChart } from "./composed-chart";
|
||||||
|
export { SeriesBar } from "./series-bar";
|
||||||
|
export { Area } from "./area";
|
||||||
|
export { Line } from "./line";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"funnel-chart": `export { FunnelChart } from "./funnel-chart";`,
|
||||||
|
"sankey-chart": `export { SankeyChart, SankeyLink, SankeyNode, SankeyTooltip } from "./sankey";`,
|
||||||
|
"candlestick-chart": `export { CandlestickChart } from "./candlestick-chart";
|
||||||
|
export { Candlestick } from "./candlestick";
|
||||||
|
export { Grid } from "./grid";
|
||||||
|
export { XAxis } from "./x-axis";
|
||||||
|
export { YAxis } from "./y-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
"choropleth-chart": `export { ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip } from "./choropleth";`,
|
||||||
|
"live-line-chart": `export { LiveLineChart } from "./live-line-chart";
|
||||||
|
export { LiveLine } from "./live-line";
|
||||||
|
export { LiveXAxis } from "./live-x-axis";
|
||||||
|
export { LiveYAxis } from "./live-y-axis";
|
||||||
|
export { ChartTooltip } from "./tooltip";`,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {Record<string, { registryDependencies: string[], dependencies: string[], importFrom: string, data: string, body: string }>} */
|
||||||
|
const EXAMPLES = {
|
||||||
|
"area-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/area-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/x-axis",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom: "AreaChart, Area, Grid, XAxis, ChartTooltip",
|
||||||
|
data: `const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), desktop: 186 },
|
||||||
|
{ date: new Date("2024-02-01"), desktop: 305 },
|
||||||
|
{ date: new Date("2024-03-01"), desktop: 237 },
|
||||||
|
{ date: new Date("2024-04-01"), desktop: 73 },
|
||||||
|
{ date: new Date("2024-05-01"), desktop: 209 },
|
||||||
|
{ date: new Date("2024-06-01"), desktop: 214 },
|
||||||
|
];`,
|
||||||
|
body: `<AreaChart data={chartData} animationDuration={1100}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Area dataKey="desktop" curve={curveNatural} strokeWidth={2.5} fillOpacity={0.4} />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</AreaChart>`,
|
||||||
|
extraImports: `import { curveNatural } from "@visx/curve";`,
|
||||||
|
},
|
||||||
|
"bar-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/bar-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/gradient@4.0.1-alpha.0",
|
||||||
|
"@visx/pattern@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom: "BarChart, Bar, BarXAxis, Grid, ChartTooltip",
|
||||||
|
data: `const data = [
|
||||||
|
{ month: "Jan", revenue: 12000, profit: 4500 },
|
||||||
|
{ month: "Feb", revenue: 15500, profit: 5200 },
|
||||||
|
{ month: "Mar", revenue: 11000, profit: 3800 },
|
||||||
|
{ month: "Apr", revenue: 18500, profit: 7100 },
|
||||||
|
{ month: "May", revenue: 16800, profit: 5400 },
|
||||||
|
{ month: "Jun", revenue: 21200, profit: 8800 },
|
||||||
|
];`,
|
||||||
|
body: `<BarChart data={data} xDataKey="month">
|
||||||
|
<Grid horizontal />
|
||||||
|
<Bar dataKey="revenue" fill="var(--chart-line-primary)" lineCap="round" />
|
||||||
|
<Bar dataKey="profit" fill="var(--chart-line-secondary)" lineCap="round" />
|
||||||
|
<BarXAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</BarChart>`,
|
||||||
|
},
|
||||||
|
"line-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/line-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/x-axis",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom: "LineChart, Line, Grid, XAxis, ChartTooltip",
|
||||||
|
data: `const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), users: 1200 },
|
||||||
|
{ date: new Date("2024-02-01"), users: 1350 },
|
||||||
|
{ date: new Date("2024-03-01"), users: 1100 },
|
||||||
|
{ date: new Date("2024-04-01"), users: 1450 },
|
||||||
|
{ date: new Date("2024-05-01"), users: 1380 },
|
||||||
|
{ date: new Date("2024-06-01"), users: 1520 },
|
||||||
|
];`,
|
||||||
|
body: `<LineChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Line dataKey="users" curve={curveNatural} stroke="var(--chart-line-primary)" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</LineChart>`,
|
||||||
|
extraImports: `import { curveNatural } from "@visx/curve";`,
|
||||||
|
},
|
||||||
|
"scatter-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/scatter-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/x-axis",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: ["d3-scale", "d3-array", "motion", "react-use-measure"],
|
||||||
|
importFrom: "ScatterChart, Scatter, Grid, XAxis, ChartTooltip",
|
||||||
|
data: `const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), sessions: 420, conversions: 28 },
|
||||||
|
{ date: new Date("2024-02-01"), sessions: 510, conversions: 34 },
|
||||||
|
{ date: new Date("2024-03-01"), sessions: 390, conversions: 22 },
|
||||||
|
{ date: new Date("2024-04-01"), sessions: 580, conversions: 41 },
|
||||||
|
{ date: new Date("2024-05-01"), sessions: 620, conversions: 38 },
|
||||||
|
{ date: new Date("2024-06-01"), sessions: 710, conversions: 52 },
|
||||||
|
];`,
|
||||||
|
body: `<ScatterChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<Scatter dataKey="sessions" />
|
||||||
|
<Scatter dataKey="conversions" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</ScatterChart>`,
|
||||||
|
},
|
||||||
|
"pie-chart": {
|
||||||
|
registryDependencies: ["@bklit/pie-chart"],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom: "PieChart, PieSlice, PieCenter",
|
||||||
|
data: `const pieData = [
|
||||||
|
{ label: "Direct", value: 320 },
|
||||||
|
{ label: "Organic", value: 280 },
|
||||||
|
{ label: "Referral", value: 190 },
|
||||||
|
{ label: "Social", value: 140 },
|
||||||
|
];`,
|
||||||
|
body: `<PieChart data={pieData} size={280}>
|
||||||
|
{pieData.map((item, i) => (
|
||||||
|
<PieSlice index={i} key={item.label} />
|
||||||
|
))}
|
||||||
|
<PieCenter defaultLabel="Traffic" />
|
||||||
|
</PieChart>`,
|
||||||
|
},
|
||||||
|
"gauge-chart": {
|
||||||
|
registryDependencies: ["@bklit/gauge-chart"],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom: "Gauge",
|
||||||
|
data: "",
|
||||||
|
body: `<Gauge
|
||||||
|
value={72}
|
||||||
|
centerValue={72}
|
||||||
|
totalNotches={40}
|
||||||
|
defaultLabel="Score"
|
||||||
|
formatOptions={{ style: "percent" }}
|
||||||
|
/>`,
|
||||||
|
},
|
||||||
|
"heatmap-chart": {
|
||||||
|
registryDependencies: ["@bklit/heatmap-chart"],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/heatmap@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom:
|
||||||
|
"HeatmapCells, HeatmapChart, HeatmapInteractionBoundary, HeatmapInteractionProvider, HeatmapLegend, HeatmapTooltip, HeatmapXAxis, HeatmapYAxis",
|
||||||
|
data: `const data = [
|
||||||
|
{
|
||||||
|
bin: 0,
|
||||||
|
bins: [
|
||||||
|
{ bin: 0, count: 2, date: new Date(2024, 0, 1) },
|
||||||
|
{ bin: 1, count: 0, date: new Date(2024, 0, 2) },
|
||||||
|
{ bin: 2, count: 3, date: new Date(2024, 0, 3) },
|
||||||
|
{ bin: 3, count: 1, date: new Date(2024, 0, 4) },
|
||||||
|
{ bin: 4, count: 4, date: new Date(2024, 0, 5) },
|
||||||
|
{ bin: 5, count: 0, date: new Date(2024, 0, 6) },
|
||||||
|
{ bin: 6, count: 1, date: new Date(2024, 0, 7) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bin: 1,
|
||||||
|
bins: [
|
||||||
|
{ bin: 0, count: 1, date: new Date(2024, 0, 8) },
|
||||||
|
{ bin: 1, count: 2, date: new Date(2024, 0, 9) },
|
||||||
|
{ bin: 2, count: 0, date: new Date(2024, 0, 10) },
|
||||||
|
{ bin: 3, count: 3, date: new Date(2024, 0, 11) },
|
||||||
|
{ bin: 4, count: 2, date: new Date(2024, 0, 12) },
|
||||||
|
{ bin: 5, count: 1, date: new Date(2024, 0, 13) },
|
||||||
|
{ bin: 6, count: 0, date: new Date(2024, 0, 14) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];`,
|
||||||
|
body: `<HeatmapInteractionProvider>
|
||||||
|
<HeatmapInteractionBoundary>
|
||||||
|
<div className="flex w-full flex-col items-stretch gap-3">
|
||||||
|
<HeatmapChart className="w-full" data={data} layout="fluid">
|
||||||
|
<HeatmapCells />
|
||||||
|
<HeatmapXAxis />
|
||||||
|
<HeatmapYAxis />
|
||||||
|
<HeatmapTooltip />
|
||||||
|
</HeatmapChart>
|
||||||
|
<HeatmapLegend />
|
||||||
|
</div>
|
||||||
|
</HeatmapInteractionBoundary>
|
||||||
|
</HeatmapInteractionProvider>`,
|
||||||
|
},
|
||||||
|
"ring-chart": {
|
||||||
|
registryDependencies: ["@bklit/ring-chart"],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom: "RingChart, Ring, RingCenter",
|
||||||
|
data: `const ringData = [
|
||||||
|
{ label: "Email", value: 42 },
|
||||||
|
{ label: "Social", value: 28 },
|
||||||
|
{ label: "Direct", value: 18 },
|
||||||
|
{ label: "Other", value: 12 },
|
||||||
|
];`,
|
||||||
|
body: `<RingChart data={ringData} size={280} strokeWidth={14}>
|
||||||
|
{ringData.map((item, i) => (
|
||||||
|
<Ring index={i} key={item.label} />
|
||||||
|
))}
|
||||||
|
<RingCenter defaultLabel="Channels" />
|
||||||
|
</RingChart>`,
|
||||||
|
},
|
||||||
|
"radar-chart": {
|
||||||
|
registryDependencies: ["@bklit/radar-chart"],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom: "RadarChart, RadarGrid, RadarAxis, RadarLabels, RadarArea",
|
||||||
|
data: `const metrics = [
|
||||||
|
{ key: "speed", label: "Speed" },
|
||||||
|
{ key: "reliability", label: "Reliability" },
|
||||||
|
{ key: "comfort", label: "Comfort" },
|
||||||
|
{ key: "safety", label: "Safety" },
|
||||||
|
{ key: "efficiency", label: "Efficiency" },
|
||||||
|
];
|
||||||
|
const data = [
|
||||||
|
{ id: "a", speed: 80, reliability: 70, comfort: 60, safety: 90, efficiency: 75 },
|
||||||
|
];`,
|
||||||
|
body: `<RadarChart data={data} metrics={metrics} size={320}>
|
||||||
|
<RadarGrid />
|
||||||
|
<RadarAxis />
|
||||||
|
<RadarLabels fontSize={10} offset={16} />
|
||||||
|
{data.map((row, i) => (
|
||||||
|
<RadarArea key={row.id} index={i} fill="var(--chart-line-primary)" fillOpacity={0.35} />
|
||||||
|
))}
|
||||||
|
</RadarChart>`,
|
||||||
|
},
|
||||||
|
"composed-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/composed-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/x-axis",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom:
|
||||||
|
"ComposedChart, SeriesBar, Area, Line, Grid, XAxis, ChartTooltip",
|
||||||
|
data: `const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), revenue: 4200, runRate: 3800 },
|
||||||
|
{ date: new Date("2024-02-01"), revenue: 5100, runRate: 4600 },
|
||||||
|
{ date: new Date("2024-03-01"), revenue: 4800, runRate: 5200 },
|
||||||
|
{ date: new Date("2024-04-01"), revenue: 5500, runRate: 5000 },
|
||||||
|
];`,
|
||||||
|
body: `<ComposedChart data={chartData}>
|
||||||
|
<Grid horizontal />
|
||||||
|
<SeriesBar dataKey="revenue" fill="var(--chart-1)" />
|
||||||
|
<Area dataKey="runRate" curve={curveNatural} fill="var(--chart-4)" fillOpacity={0.35} />
|
||||||
|
<Line dataKey="runRate" curve={curveNatural} stroke="var(--chart-2)" />
|
||||||
|
<XAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</ComposedChart>`,
|
||||||
|
extraImports: `import { curveNatural } from "@visx/curve";`,
|
||||||
|
},
|
||||||
|
"funnel-chart": {
|
||||||
|
registryDependencies: ["@bklit/funnel-chart"],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom: "FunnelChart",
|
||||||
|
data: `const funnelData = [
|
||||||
|
{ label: "Visitors", value: 12000 },
|
||||||
|
{ label: "Signups", value: 4800 },
|
||||||
|
{ label: "Activated", value: 2100 },
|
||||||
|
{ label: "Paid", value: 840 },
|
||||||
|
];`,
|
||||||
|
body: `<FunnelChart data={funnelData} aspectRatio="2 / 1" />`,
|
||||||
|
},
|
||||||
|
"sankey-chart": {
|
||||||
|
registryDependencies: ["@bklit/sankey-chart"],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/sankey@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom: "SankeyChart, SankeyLink, SankeyNode, SankeyTooltip",
|
||||||
|
data: `const data = {
|
||||||
|
nodes: [
|
||||||
|
{ name: "Ads" },
|
||||||
|
{ name: "Organic" },
|
||||||
|
{ name: "Landing" },
|
||||||
|
{ name: "Product" },
|
||||||
|
{ name: "Checkout" },
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ source: 0, target: 2, value: 40 },
|
||||||
|
{ source: 1, target: 2, value: 30 },
|
||||||
|
{ source: 2, target: 3, value: 50 },
|
||||||
|
{ source: 3, target: 4, value: 35 },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
body: `<SankeyChart data={data} aspectRatio="16 / 9">
|
||||||
|
<SankeyLink />
|
||||||
|
<SankeyNode />
|
||||||
|
<SankeyTooltip />
|
||||||
|
</SankeyChart>`,
|
||||||
|
},
|
||||||
|
"candlestick-chart": {
|
||||||
|
registryDependencies: [
|
||||||
|
"@bklit/candlestick-chart",
|
||||||
|
"@bklit/grid",
|
||||||
|
"@bklit/x-axis",
|
||||||
|
"@bklit/y-axis",
|
||||||
|
"@bklit/chart-tooltip",
|
||||||
|
],
|
||||||
|
dependencies: ["@visx/shape@4.0.1-alpha.0", "motion"],
|
||||||
|
importFrom:
|
||||||
|
"CandlestickChart, Candlestick, Grid, XAxis, YAxis, ChartTooltip",
|
||||||
|
data: `const ohlcData = [
|
||||||
|
{ date: new Date("2024-01-02"), open: 100, high: 108, low: 98, close: 105 },
|
||||||
|
{ date: new Date("2024-01-03"), open: 105, high: 110, low: 102, close: 103 },
|
||||||
|
{ date: new Date("2024-01-04"), open: 103, high: 112, low: 101, close: 110 },
|
||||||
|
{ date: new Date("2024-01-05"), open: 110, high: 115, low: 107, close: 108 },
|
||||||
|
];`,
|
||||||
|
body: `<CandlestickChart data={ohlcData}>
|
||||||
|
<Grid horizontal vertical />
|
||||||
|
<Candlestick />
|
||||||
|
<XAxis />
|
||||||
|
<YAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</CandlestickChart>`,
|
||||||
|
},
|
||||||
|
"choropleth-chart": {
|
||||||
|
registryDependencies: ["@bklit/choropleth-chart"],
|
||||||
|
dependencies: ["@visx/geo@4.0.1-alpha.0", "d3-geo", "topojson-client"],
|
||||||
|
importFrom:
|
||||||
|
"ChoroplethChart, ChoroplethFeatureComponent, ChoroplethTooltip",
|
||||||
|
data: `const features = [
|
||||||
|
{ id: "US", name: "United States", value: 120 },
|
||||||
|
{ id: "CA", name: "Canada", value: 45 },
|
||||||
|
{ id: "GB", name: "United Kingdom", value: 62 },
|
||||||
|
];`,
|
||||||
|
body: `<ChoroplethChart data={features} aspectRatio="2 / 1">
|
||||||
|
<ChoroplethFeatureComponent />
|
||||||
|
<ChoroplethTooltip />
|
||||||
|
</ChoroplethChart>`,
|
||||||
|
},
|
||||||
|
"live-line-chart": {
|
||||||
|
registryDependencies: ["@bklit/live-line-chart", "@bklit/chart-tooltip"],
|
||||||
|
dependencies: [
|
||||||
|
"@visx/curve@4.0.1-alpha.0",
|
||||||
|
"@visx/shape@4.0.1-alpha.0",
|
||||||
|
"motion",
|
||||||
|
],
|
||||||
|
importFrom: "LiveLineChart, LiveLine, LiveXAxis, LiveYAxis, ChartTooltip",
|
||||||
|
data: `const initialData = Array.from({ length: 24 }, (_, i) => ({
|
||||||
|
time: Date.now() - (23 - i) * 60_000,
|
||||||
|
value: 50 + Math.sin(i / 3) * 20,
|
||||||
|
}));`,
|
||||||
|
body: `<LiveLineChart
|
||||||
|
data={initialData}
|
||||||
|
interval={1000}
|
||||||
|
maxPoints={24}
|
||||||
|
xDataKey="time"
|
||||||
|
yDataKey="value"
|
||||||
|
>
|
||||||
|
<LiveLine curve={curveNatural} stroke="var(--chart-line-primary)" />
|
||||||
|
<LiveXAxis />
|
||||||
|
<LiveYAxis />
|
||||||
|
<ChartTooltip />
|
||||||
|
</LiveLineChart>`,
|
||||||
|
extraImports: `import { curveNatural } from "@visx/curve";`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mkdirSync(examplesDir, { recursive: true });
|
||||||
|
|
||||||
|
for (const [slug, spec] of Object.entries(EXAMPLES)) {
|
||||||
|
const extra = spec.extraImports ? `${spec.extraImports}\n\n` : "";
|
||||||
|
const content = `"use client"
|
||||||
|
|
||||||
|
// In your app (monorepo/npm): import { ${spec.importFrom} } from "@bklitui/ui/charts"
|
||||||
|
import { ${spec.importFrom} } from "@/components/charts"
|
||||||
|
|
||||||
|
${extra}${spec.data}
|
||||||
|
|
||||||
|
export default function Component() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
${spec.body}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
writeFileSync(join(examplesDir, `${slug}.tsx`), content);
|
||||||
|
|
||||||
|
const indexExports = CHART_INDEX_EXPORTS[slug];
|
||||||
|
if (indexExports) {
|
||||||
|
writeFileSync(
|
||||||
|
join(examplesDir, `${slug}-index.ts`),
|
||||||
|
`// biome-ignore-all lint/performance/noBarrelFile: v0 registry example barrel for shadcn install\n${indexExports}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryPath = join(root, "registry.json");
|
||||||
|
const registry = JSON.parse(readFileSync(registryPath, "utf8"));
|
||||||
|
|
||||||
|
registry.items = registry.items.filter(
|
||||||
|
(item) => !item.name.endsWith("-example")
|
||||||
|
);
|
||||||
|
|
||||||
|
const exampleItems = Object.entries(EXAMPLES).map(([slug, spec]) => {
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
path: `registry/examples/${slug}.tsx`,
|
||||||
|
type: "registry:page",
|
||||||
|
target: "app/page.tsx",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (CHART_INDEX_EXPORTS[slug]) {
|
||||||
|
files.push({
|
||||||
|
path: `registry/examples/${slug}-index.ts`,
|
||||||
|
type: "registry:lib",
|
||||||
|
target: "components/charts/index.ts",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: `${slug}-example`,
|
||||||
|
type: "registry:example",
|
||||||
|
title: `${slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} Example`,
|
||||||
|
description: `Composable ${slug} demo for Open in v0`,
|
||||||
|
registryDependencies: spec.registryDependencies,
|
||||||
|
dependencies: spec.dependencies,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.items.push(...exampleItems);
|
||||||
|
|
||||||
|
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`);
|
||||||
|
console.log(`Wrote ${Object.keys(EXAMPLES).length} v0 example registry items`);
|
||||||
24
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/run-tests.mjs
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { readdirSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const testDir = join(packageRoot, "src/charts/__tests__");
|
||||||
|
|
||||||
|
const tests = readdirSync(testDir)
|
||||||
|
.filter((name) => name.endsWith(".test.ts"))
|
||||||
|
.map((name) => join(testDir, name));
|
||||||
|
|
||||||
|
if (tests.length === 0) {
|
||||||
|
console.error(`No test files found in ${testDir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
["--import", "tsx", "--test", ...tests],
|
||||||
|
{ stdio: "inherit", cwd: packageRoot }
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
169
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-deps.mjs
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Verifies registry item `dependencies` cover all npm imports in the item's
|
||||||
|
* file tree plus transitive @bklit registryDependencies (shadcn / v0 install closure).
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const uiRoot = path.join(__dirname, "..");
|
||||||
|
const registryPath = path.join(uiRoot, "registry.json");
|
||||||
|
|
||||||
|
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
||||||
|
const byName = Object.fromEntries(
|
||||||
|
registry.items.map((item) => [item.name, item])
|
||||||
|
);
|
||||||
|
|
||||||
|
const importRe =
|
||||||
|
/(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g;
|
||||||
|
const DEP_VERSION_SUFFIX_RE = /@[\d.]+(-[\w.]+)?$/;
|
||||||
|
const BKLIT_REGISTRY_DEP_RE = /^@bklit\/(.+)$/;
|
||||||
|
|
||||||
|
function getPackageName(spec) {
|
||||||
|
if (spec.startsWith(".") || spec.startsWith("@/")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (spec.startsWith("@")) {
|
||||||
|
const parts = spec.split("/");
|
||||||
|
return `${parts[0]}/${parts[1]}`;
|
||||||
|
}
|
||||||
|
return spec.split("/")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDep(dep) {
|
||||||
|
return dep.replace(DEP_VERSION_SUFFIX_RE, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import specifiers that are satisfied by @types/* registry dependencies. */
|
||||||
|
const TYPES_ALIASES = {
|
||||||
|
geojson: ["geojson", "@types/geojson"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDeclared(pkg, declared) {
|
||||||
|
if (declared.has(pkg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const aliases = TYPES_ALIASES[pkg];
|
||||||
|
if (aliases) {
|
||||||
|
return aliases.some((name) => declared.has(name));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanFiles(files) {
|
||||||
|
const used = new Set();
|
||||||
|
for (const file of files ?? []) {
|
||||||
|
const fullPath = path.join(uiRoot, file.path);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`Registry file not found: ${file.path}`);
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(fullPath, "utf8");
|
||||||
|
importRe.lastIndex = 0;
|
||||||
|
let match = importRe.exec(content);
|
||||||
|
while (match) {
|
||||||
|
const pkg = getPackageName(match[1]);
|
||||||
|
if (pkg && pkg !== "react" && pkg !== "react-dom") {
|
||||||
|
used.add(pkg);
|
||||||
|
}
|
||||||
|
match = importRe.exec(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegistryDepName(dep) {
|
||||||
|
const m = dep.match(BKLIT_REGISTRY_DEP_RE);
|
||||||
|
return m ? m[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDeclared(name, visited = new Set()) {
|
||||||
|
const declared = new Set();
|
||||||
|
const visit = (itemName) => {
|
||||||
|
if (visited.has(itemName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(itemName);
|
||||||
|
const item = byName[itemName];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const dep of item.dependencies ?? []) {
|
||||||
|
declared.add(normalizeDep(dep));
|
||||||
|
}
|
||||||
|
for (const regDep of item.registryDependencies ?? []) {
|
||||||
|
const nested = getRegistryDepName(regDep);
|
||||||
|
if (nested) {
|
||||||
|
visit(nested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(name);
|
||||||
|
return declared;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUsed(name, visited = new Set()) {
|
||||||
|
const used = new Set();
|
||||||
|
const visit = (itemName) => {
|
||||||
|
if (visited.has(itemName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(itemName);
|
||||||
|
const item = byName[itemName];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const pkg of scanFiles(item.files)) {
|
||||||
|
used.add(pkg);
|
||||||
|
}
|
||||||
|
for (const regDep of item.registryDependencies ?? []) {
|
||||||
|
const nested = getRegistryDepName(regDep);
|
||||||
|
if (nested) {
|
||||||
|
visit(nested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(name);
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartItems = registry.items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.type === "registry:component" &&
|
||||||
|
(item.name.endsWith("-chart") ||
|
||||||
|
item.name === "chart-context" ||
|
||||||
|
item.name === "chart-animation")
|
||||||
|
);
|
||||||
|
|
||||||
|
const utilityItems = registry.items.filter((item) =>
|
||||||
|
["grid", "x-axis", "y-axis", "chart-tooltip", "legend", "markers"].includes(
|
||||||
|
item.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
for (const item of [...chartItems, ...utilityItems]) {
|
||||||
|
const declared = collectDeclared(item.name);
|
||||||
|
const used = collectUsed(item.name);
|
||||||
|
const missing = [...used].filter((pkg) => !isDeclared(pkg, declared)).sort();
|
||||||
|
if (missing.length > 0) {
|
||||||
|
failures.push({ name: item.name, missing });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error("Registry dependency closure check failed:\n");
|
||||||
|
for (const { name, missing } of failures) {
|
||||||
|
console.error(` ${name}: missing ${missing.join(", ")}`);
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
"\nAdd missing packages to `dependencies` in packages/ui/registry.json (on the item or a registryDependency)."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registry dependency closure OK (${chartItems.length + utilityItems.length} items checked).`
|
||||||
|
);
|
||||||
178
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-files.mjs
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Verifies every installable registry item ships all local `./` imports in its
|
||||||
|
* transitive file closure (item files + registryDependencies).
|
||||||
|
*
|
||||||
|
* Also rejects CommonJS `require("react-dom")` in registry source files — it
|
||||||
|
* breaks Vite/ESM consumers even though Next.js/webpack may tolerate it.
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const uiRoot = path.join(__dirname, "..");
|
||||||
|
const registryPath = path.join(uiRoot, "registry.json");
|
||||||
|
|
||||||
|
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
||||||
|
const byName = Object.fromEntries(
|
||||||
|
registry.items.map((item) => [item.name, item])
|
||||||
|
);
|
||||||
|
|
||||||
|
const importRe =
|
||||||
|
/(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"](\.[^'"]+)['"]/g;
|
||||||
|
const BKLIT_REGISTRY_DEP_RE = /^@bklit\/(.+)$/;
|
||||||
|
|
||||||
|
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
||||||
|
|
||||||
|
function resolveRelativeImport(fromFilePath, spec) {
|
||||||
|
const dir = path.dirname(fromFilePath);
|
||||||
|
const base = path.normalize(path.join(dir, spec));
|
||||||
|
const candidates = [
|
||||||
|
base,
|
||||||
|
...RESOLVE_EXTENSIONS.map((ext) => base + ext),
|
||||||
|
...RESOLVE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const fullPath = path.join(uiRoot, candidate);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectItemFiles(itemName, visited = new Set()) {
|
||||||
|
const files = new Set();
|
||||||
|
const visit = (name) => {
|
||||||
|
if (visited.has(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(name);
|
||||||
|
const item = byName[name];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const file of item.files ?? []) {
|
||||||
|
files.add(file.path);
|
||||||
|
}
|
||||||
|
for (const regDep of item.registryDependencies ?? []) {
|
||||||
|
const nested = regDep.match(BKLIT_REGISTRY_DEP_RE)?.[1];
|
||||||
|
if (nested) {
|
||||||
|
visit(nested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(itemName);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanRelativeImports(filePath) {
|
||||||
|
// v0/shadcn example barrels re-export co-installed chart files from the
|
||||||
|
// target `components/charts/` directory — not from registry source paths.
|
||||||
|
if (
|
||||||
|
filePath.startsWith("registry/examples/") &&
|
||||||
|
filePath.endsWith("-index.ts")
|
||||||
|
) {
|
||||||
|
return { imports: [], content: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(uiRoot, filePath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
throw new Error(`Registry file not found: ${filePath}`);
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(fullPath, "utf8");
|
||||||
|
const imports = [];
|
||||||
|
importRe.lastIndex = 0;
|
||||||
|
let match = importRe.exec(content);
|
||||||
|
while (match) {
|
||||||
|
imports.push(match[1]);
|
||||||
|
match = importRe.exec(content);
|
||||||
|
}
|
||||||
|
return { imports, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
const INSTALLABLE_TYPES = new Set([
|
||||||
|
"registry:component",
|
||||||
|
"registry:lib",
|
||||||
|
"registry:block",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const installableItems = registry.items.filter((item) =>
|
||||||
|
INSTALLABLE_TYPES.has(item.type)
|
||||||
|
);
|
||||||
|
|
||||||
|
const closureFailures = [];
|
||||||
|
const requireFailures = [];
|
||||||
|
|
||||||
|
for (const item of installableItems) {
|
||||||
|
const closure = collectItemFiles(item.name);
|
||||||
|
const missing = new Map();
|
||||||
|
|
||||||
|
for (const filePath of closure) {
|
||||||
|
const { imports, content } = scanRelativeImports(filePath);
|
||||||
|
|
||||||
|
if (content.includes('require("react-dom")')) {
|
||||||
|
requireFailures.push({ item: item.name, file: filePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const spec of imports) {
|
||||||
|
const resolved = resolveRelativeImport(filePath, spec);
|
||||||
|
if (!resolved) {
|
||||||
|
const key = `${filePath} -> ${spec}`;
|
||||||
|
if (!missing.has(key)) {
|
||||||
|
missing.set(key, "missing on disk");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!closure.has(resolved)) {
|
||||||
|
const key = `${filePath} -> ${spec} (${resolved})`;
|
||||||
|
if (!missing.has(key)) {
|
||||||
|
missing.set(key, "not in install closure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.size > 0) {
|
||||||
|
closureFailures.push({ name: item.name, missing: [...missing.entries()] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
|
||||||
|
if (closureFailures.length > 0) {
|
||||||
|
failed = true;
|
||||||
|
console.error("Registry local import closure check failed:\n");
|
||||||
|
for (const { name, missing } of closureFailures) {
|
||||||
|
console.error(` ${name}:`);
|
||||||
|
for (const [ref, reason] of missing) {
|
||||||
|
console.error(` - ${ref} (${reason})`);
|
||||||
|
}
|
||||||
|
console.error("");
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
"Add missing files to packages/ui/registry.json (on the item or a registryDependency)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireFailures.length > 0) {
|
||||||
|
failed = true;
|
||||||
|
console.error('Registry source must not use require("react-dom"):\n');
|
||||||
|
for (const { item, file } of requireFailures) {
|
||||||
|
console.error(` ${item}: ${file}`);
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
'\nUse `import { createPortal } from "react-dom"` in "use client" components instead.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Registry file closure OK (${installableItems.length} installable items checked).`
|
||||||
|
);
|
||||||
139
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/scripts/verify-registry-sync.mjs
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Ensures shadcn registry output matches chart source when packages/ui/src/charts changes.
|
||||||
|
* Run via `pnpm registry:verify` (locally or in CI).
|
||||||
|
*/
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = join(__dirname, "../../..");
|
||||||
|
|
||||||
|
const CHARTS_SRC = "packages/ui/src/charts";
|
||||||
|
const REGISTRY_PATHS = [
|
||||||
|
"apps/web/public/r",
|
||||||
|
"packages/ui/registry.json",
|
||||||
|
"packages/ui/registry/examples",
|
||||||
|
"packages/ui/registry/blocks",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Build scripts may reformat these; compare parsed JSON, not raw text. */
|
||||||
|
const SEMANTIC_JSON_PATHS = [
|
||||||
|
"packages/ui/registry.json",
|
||||||
|
"apps/web/public/r/registry.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
function run(cmd, { inherit = false } = {}) {
|
||||||
|
return execSync(cmd, {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: inherit ? "inherit" : "pipe",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJson(relativePath) {
|
||||||
|
return JSON.parse(fs.readFileSync(join(repoRoot, relativePath), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonEqual(a, b) {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDiffRange() {
|
||||||
|
if (process.env.REGISTRY_SYNC_DIFF_RANGE) {
|
||||||
|
return process.env.REGISTRY_SYNC_DIFF_RANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.GITHUB_EVENT_NAME === "pull_request") {
|
||||||
|
const base = process.env.GITHUB_BASE_REF || "main";
|
||||||
|
return `origin/${base}...HEAD`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.env.GITHUB_EVENT_NAME === "push" &&
|
||||||
|
process.env.GITHUB_EVENT_BEFORE &&
|
||||||
|
process.env.GITHUB_SHA
|
||||||
|
) {
|
||||||
|
return `${process.env.GITHUB_EVENT_BEFORE}...${process.env.GITHUB_SHA}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "origin/main...HEAD";
|
||||||
|
}
|
||||||
|
|
||||||
|
function chartsChanged(diffRange) {
|
||||||
|
const changed = run(
|
||||||
|
`git diff --name-only ${diffRange} -- ${CHARTS_SRC}`
|
||||||
|
).trim();
|
||||||
|
return changed.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listDirtyRegistryFiles() {
|
||||||
|
const excludeArgs = SEMANTIC_JSON_PATHS.map((p) => `':!${p}'`).join(" ");
|
||||||
|
const paths = REGISTRY_PATHS.join(" ");
|
||||||
|
const status = run(
|
||||||
|
`git status --porcelain -- ${paths} ${excludeArgs}`
|
||||||
|
).trim();
|
||||||
|
if (!status) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return status.split("\n").filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function listSemanticRegistryDrift(beforeByPath) {
|
||||||
|
const drift = [];
|
||||||
|
for (const relativePath of SEMANTIC_JSON_PATHS) {
|
||||||
|
const after = readJson(relativePath);
|
||||||
|
if (!jsonEqual(beforeByPath[relativePath], after)) {
|
||||||
|
drift.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return drift;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const diffRange = resolveDiffRange();
|
||||||
|
|
||||||
|
if (!chartsChanged(diffRange)) {
|
||||||
|
console.log(
|
||||||
|
`No changes in ${CHARTS_SRC} (${diffRange}); registry sync check skipped.`
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Chart source changed (${diffRange}); rebuilding registry to verify sync...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const registryBefore = Object.fromEntries(
|
||||||
|
SEMANTIC_JSON_PATHS.map((relativePath) => [
|
||||||
|
relativePath,
|
||||||
|
readJson(relativePath),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
run("pnpm registry:build", { inherit: true });
|
||||||
|
|
||||||
|
const semanticDrift = listSemanticRegistryDrift(registryBefore);
|
||||||
|
const dirty = listDirtyRegistryFiles();
|
||||||
|
const failures = [
|
||||||
|
...semanticDrift.map((path) => `${path} (content changed)`),
|
||||||
|
...dirty,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error("\nRegistry is out of sync with chart source changes.\n");
|
||||||
|
console.error(
|
||||||
|
"Run `pnpm registry:build` from the repo root and commit the updated registry files:\n"
|
||||||
|
);
|
||||||
|
for (const line of failures) {
|
||||||
|
console.error(` ${line}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Registry is in sync with chart source.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
appName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = ({ children, className, appName }: ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={className}
|
||||||
|
onClick={() => console.log(`Hello from your ${appName} app!`)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export function Card({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
href,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={className}
|
||||||
|
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
{title} <span>-></span>
|
||||||
|
</h2>
|
||||||
|
<p>{children}</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GridColumns, GridRows } from "@visx/grid";
|
||||||
|
import type { scaleLinear, scaleTime } from "@visx/scale";
|
||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
|
type ScaleLinear<Output, _Input = number> = ReturnType<
|
||||||
|
typeof scaleLinear<Output>
|
||||||
|
>;
|
||||||
|
type ScaleTime<Output, _Input = Date | number> = ReturnType<
|
||||||
|
typeof scaleTime<Output>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ChartGridProps {
|
||||||
|
/** Width of the grid area */
|
||||||
|
width: number;
|
||||||
|
/** Height of the grid area */
|
||||||
|
height: number;
|
||||||
|
/** X scale (time scale) */
|
||||||
|
xScale: ScaleTime<number, number>;
|
||||||
|
/** Y scale (linear scale) */
|
||||||
|
yScale: ScaleLinear<number, number>;
|
||||||
|
/** Show horizontal grid lines. Default: true */
|
||||||
|
showRows?: boolean;
|
||||||
|
/** Show vertical grid lines. Default: false */
|
||||||
|
showColumns?: boolean;
|
||||||
|
/** Number of horizontal grid lines. Default: 5 */
|
||||||
|
numTicksRows?: number;
|
||||||
|
/** Number of vertical grid lines. Default: 10 */
|
||||||
|
numTicksColumns?: number;
|
||||||
|
/** Grid line stroke color. Default: var(--chart-grid) */
|
||||||
|
stroke?: string;
|
||||||
|
/** Grid line stroke opacity. Default: 1 */
|
||||||
|
strokeOpacity?: number;
|
||||||
|
/** Grid line stroke width. Default: 1 */
|
||||||
|
strokeWidth?: number;
|
||||||
|
/** Grid line dash array. Default: "4,4" for dashed lines */
|
||||||
|
strokeDasharray?: string;
|
||||||
|
/** Enable horizontal fade effect on grid rows. Default: true */
|
||||||
|
fadeRows?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChartGrid({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
xScale,
|
||||||
|
yScale,
|
||||||
|
showRows = true,
|
||||||
|
showColumns = false,
|
||||||
|
numTicksRows = 5,
|
||||||
|
numTicksColumns = 10,
|
||||||
|
stroke = "var(--chart-grid)",
|
||||||
|
strokeOpacity = 1,
|
||||||
|
strokeWidth = 1,
|
||||||
|
strokeDasharray = "4,4",
|
||||||
|
fadeRows = true,
|
||||||
|
}: ChartGridProps) {
|
||||||
|
const uniqueId = useId();
|
||||||
|
const maskId = `chart-grid-rows-fade-${uniqueId}`;
|
||||||
|
const gradientId = `${maskId}-gradient`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g className="chart-grid">
|
||||||
|
{/* Gradient mask for horizontal grid lines - fades at both ends */}
|
||||||
|
{showRows && fadeRows && (
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={gradientId} x1="0%" x2="100%" y1="0%" y2="0%">
|
||||||
|
<stop offset="0%" style={{ stopColor: "white", stopOpacity: 0 }} />
|
||||||
|
<stop offset="10%" style={{ stopColor: "white", stopOpacity: 1 }} />
|
||||||
|
<stop offset="90%" style={{ stopColor: "white", stopOpacity: 1 }} />
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style={{ stopColor: "white", stopOpacity: 0 }}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<mask id={maskId}>
|
||||||
|
<rect
|
||||||
|
fill={`url(#${gradientId})`}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRows && (
|
||||||
|
<g mask={fadeRows ? `url(#${maskId})` : undefined}>
|
||||||
|
<GridRows
|
||||||
|
numTicks={numTicksRows}
|
||||||
|
scale={yScale}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
{showColumns && (
|
||||||
|
<GridColumns
|
||||||
|
height={height}
|
||||||
|
numTicks={numTicksColumns}
|
||||||
|
scale={xScale}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeOpacity={strokeOpacity}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChartGrid;
|
||||||
38
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/__tests__/animation.test.ts
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
clipRevealTransition,
|
||||||
|
DEFAULT_CHART_ENTER_TRANSITION,
|
||||||
|
} from "../animation";
|
||||||
|
|
||||||
|
describe("clipRevealTransition", () => {
|
||||||
|
it("preserves explicit tween ease", () => {
|
||||||
|
const tween = {
|
||||||
|
type: "tween" as const,
|
||||||
|
duration: 0.9,
|
||||||
|
ease: [0.1, 0.2, 0.3, 0.4] as [number, number, number, number],
|
||||||
|
};
|
||||||
|
assert.deepEqual(clipRevealTransition(tween), tween);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies default ease when tween omits ease", () => {
|
||||||
|
const result = clipRevealTransition({
|
||||||
|
type: "tween",
|
||||||
|
duration: 0.9,
|
||||||
|
});
|
||||||
|
assert.equal(result.type, "tween");
|
||||||
|
assert.equal(result.duration, 0.9);
|
||||||
|
assert.deepEqual(result.ease, DEFAULT_CHART_ENTER_TRANSITION.ease);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts spring to tween for svg width reveal", () => {
|
||||||
|
const result = clipRevealTransition({
|
||||||
|
type: "spring",
|
||||||
|
duration: 1.2,
|
||||||
|
bounce: 0.5,
|
||||||
|
});
|
||||||
|
assert.equal(result.type, "tween");
|
||||||
|
assert.equal(result.duration, 1.2);
|
||||||
|
assert.deepEqual(result.ease, DEFAULT_CHART_ENTER_TRANSITION.ease);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
BAR_DEPTH_MAX_PX,
|
||||||
|
BAR_DEPTH_PERSPECTIVE_RATIO,
|
||||||
|
barDepthAndRise,
|
||||||
|
barDepthMaxDepth,
|
||||||
|
} from "../bar-depth-geometry";
|
||||||
|
|
||||||
|
describe("barDepthMaxDepth", () => {
|
||||||
|
it("caps at BAR_DEPTH_MAX_PX when the gap is wide", () => {
|
||||||
|
// step 100, band 90 → gap 10, gap-1=9; band*0.22=19.8 → min(19.8, 9, 7) = 7
|
||||||
|
assert.equal(barDepthMaxDepth(100, 90), BAR_DEPTH_MAX_PX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is bounded by the inter-bar gap on dense charts", () => {
|
||||||
|
// step 50, band 48 → gap 2, gap-1=1; band*0.22=10.56 → min(10.56, 1, 7) = 1
|
||||||
|
assert.equal(barDepthMaxDepth(50, 48), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is 0 when there is no gap between bars", () => {
|
||||||
|
assert.equal(barDepthMaxDepth(50, 50), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("barDepthAndRise", () => {
|
||||||
|
it("returns no depth for a dead-center bar (absOffset 0)", () => {
|
||||||
|
assert.deepEqual(barDepthAndRise(0, 100, 7), {
|
||||||
|
depth: 0,
|
||||||
|
perspectiveRise: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scales depth with offset, capped by maxDepth at the edge", () => {
|
||||||
|
assert.equal(barDepthAndRise(1, 100, 7).depth, 7);
|
||||||
|
assert.equal(barDepthAndRise(0.5, 100, 7).depth, 3.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps depth by the bar height so short bars stay proportional", () => {
|
||||||
|
// naturalHeight 4 < maxDepth 7 → depth capped at 4
|
||||||
|
assert.equal(barDepthAndRise(1, 4, 7).depth, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives the rise from depth via the perspective ratio", () => {
|
||||||
|
const { depth, perspectiveRise } = barDepthAndRise(0.5, 100, 7);
|
||||||
|
assert.equal(perspectiveRise, depth * BAR_DEPTH_PERSPECTIVE_RATIO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps out-of-range inputs defensively", () => {
|
||||||
|
assert.equal(barDepthAndRise(2, 100, 7).depth, 7); // absOffset clamped to 1
|
||||||
|
assert.equal(barDepthAndRise(-1, 100, 7).depth, 0); // clamped to 0
|
||||||
|
assert.equal(barDepthAndRise(1, -5, 7).depth, 0); // negative height → 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
hmsTimeFmt,
|
||||||
|
intFmt,
|
||||||
|
shortDateFmt,
|
||||||
|
weekdayDateFmt,
|
||||||
|
} from "../chart-formatters";
|
||||||
|
|
||||||
|
const sampleDates = [
|
||||||
|
new Date(2025, 0, 5, 9, 8, 7),
|
||||||
|
new Date(2024, 11, 31, 23, 59, 59),
|
||||||
|
new Date(2026, 6, 4, 12, 0, 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
const sampleNumbers = [0, 42, 1234, 1_234_567, -9876.5];
|
||||||
|
|
||||||
|
describe("chart-formatters", () => {
|
||||||
|
describe("shortDateFmt", () => {
|
||||||
|
for (const date of sampleDates) {
|
||||||
|
it(`matches toLocaleDateString for ${date.toISOString()}`, () => {
|
||||||
|
assert.equal(
|
||||||
|
shortDateFmt.format(date),
|
||||||
|
date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("weekdayDateFmt", () => {
|
||||||
|
for (const date of sampleDates) {
|
||||||
|
it(`matches toLocaleDateString for ${date.toISOString()}`, () => {
|
||||||
|
assert.equal(
|
||||||
|
weekdayDateFmt.format(date),
|
||||||
|
date.toLocaleDateString("en-US", {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hmsTimeFmt", () => {
|
||||||
|
for (const date of sampleDates) {
|
||||||
|
it(`matches toLocaleTimeString for ${date.toISOString()}`, () => {
|
||||||
|
assert.equal(
|
||||||
|
hmsTimeFmt.format(date),
|
||||||
|
date.toLocaleTimeString("en-US", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("intFmt", () => {
|
||||||
|
for (const value of sampleNumbers) {
|
||||||
|
it(`matches toLocaleString for ${value}`, () => {
|
||||||
|
assert.equal(intFmt(value), value.toLocaleString("en-US"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("is a reusable formatter function", () => {
|
||||||
|
const formatValue = intFmt;
|
||||||
|
assert.equal(formatValue(1000), (1000).toLocaleString("en-US"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
decimateOhlcData,
|
||||||
|
decimateTimeSeries,
|
||||||
|
maxRenderPointsForWidth,
|
||||||
|
} from "../decimate-time-series";
|
||||||
|
|
||||||
|
describe("decimateTimeSeries", () => {
|
||||||
|
it("returns the original array when under the point budget", () => {
|
||||||
|
const data = [{ v: 1 }, { v: 2 }, { v: 3 }];
|
||||||
|
assert.equal(decimateTimeSeries(data, 10), data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always keeps the first and last points", () => {
|
||||||
|
const data = Array.from({ length: 100 }, (_, i) => ({ v: i }));
|
||||||
|
const sampled = decimateTimeSeries(data, 20, ["v"]);
|
||||||
|
assert.equal(sampled[0]?.v, 0);
|
||||||
|
assert.equal(sampled.at(-1)?.v, 99);
|
||||||
|
assert.equal(sampled.length, 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves spikes in the series", () => {
|
||||||
|
const data = Array.from({ length: 50 }, (_, i) => ({
|
||||||
|
v: i === 25 ? 1000 : i,
|
||||||
|
}));
|
||||||
|
const sampled = decimateTimeSeries(data, 10, ["v"]);
|
||||||
|
assert(sampled.some((point) => point.v === 1000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decimateOhlcData", () => {
|
||||||
|
it("preserves bucket high/low extremes", () => {
|
||||||
|
const data = Array.from({ length: 100 }, (_, i) => ({
|
||||||
|
date: new Date(i),
|
||||||
|
open: i,
|
||||||
|
high: i === 50 ? 999 : i + 5,
|
||||||
|
low: i === 50 ? 1 : i - 5,
|
||||||
|
close: i + 1,
|
||||||
|
}));
|
||||||
|
const sampled = decimateOhlcData(data, 10);
|
||||||
|
assert(sampled.some((row) => row.high === 999));
|
||||||
|
assert(sampled.some((row) => row.low === 1));
|
||||||
|
assert.equal(sampled.length, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("maxRenderPointsForWidth", () => {
|
||||||
|
it("returns at least 64 points", () => {
|
||||||
|
assert.equal(maxRenderPointsForWidth(10), 64);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scales with chart width", () => {
|
||||||
|
assert.equal(maxRenderPointsForWidth(400), 600);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { computeSegmentBounds } from "../highlight-segment-bounds";
|
||||||
|
|
||||||
|
// Data points at t = 0,10,20,30 ms; a linear pixel xScale (1px per ms) keeps the
|
||||||
|
// arithmetic obvious — the band is the pixel x-range one data point either side.
|
||||||
|
const data = [{ t: 0 }, { t: 10 }, { t: 20 }, { t: 30 }];
|
||||||
|
const xAccessor = (d: Record<string, unknown>) => new Date(d.t as number);
|
||||||
|
const xScale = (value: Date) => value.getTime();
|
||||||
|
|
||||||
|
describe("computeSegmentBounds", () => {
|
||||||
|
it("is inactive for empty data", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
computeSegmentBounds([], xScale, xAccessor, { index: 1 }, null),
|
||||||
|
{ x: 0, width: 0, isActive: false }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is inactive with no hover and no selection", () => {
|
||||||
|
assert.equal(
|
||||||
|
computeSegmentBounds(data, xScale, xAccessor, null, null).isActive,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("spans one data point either side of the hovered index", () => {
|
||||||
|
// idx 1 → [t 0, t 20] → x 0, width 20
|
||||||
|
assert.deepEqual(
|
||||||
|
computeSegmentBounds(data, xScale, xAccessor, { index: 1 }, null),
|
||||||
|
{ x: 0, width: 20, isActive: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps the start at the first index", () => {
|
||||||
|
// idx 0 → [t 0, t 10] → x 0, width 10
|
||||||
|
assert.deepEqual(
|
||||||
|
computeSegmentBounds(data, xScale, xAccessor, { index: 0 }, null),
|
||||||
|
{ x: 0, width: 10, isActive: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps the end at the last index", () => {
|
||||||
|
// idx 3 (last) → [t 20, t 30] → x 20, width 10
|
||||||
|
assert.deepEqual(
|
||||||
|
computeSegmentBounds(data, xScale, xAccessor, { index: 3 }, null),
|
||||||
|
{ x: 20, width: 10, isActive: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the dragged pixel range for an active selection (takes priority)", () => {
|
||||||
|
const bounds = computeSegmentBounds(
|
||||||
|
data,
|
||||||
|
xScale,
|
||||||
|
xAccessor,
|
||||||
|
{ index: 1 }, // ignored — selection wins
|
||||||
|
{ active: true, startX: 5, endX: 25 }
|
||||||
|
);
|
||||||
|
assert.deepEqual(bounds, { x: 5, width: 20, isActive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes a reversed (right-to-left) selection drag", () => {
|
||||||
|
const bounds = computeSegmentBounds(data, xScale, xAccessor, null, {
|
||||||
|
active: true,
|
||||||
|
startX: 25,
|
||||||
|
endX: 5,
|
||||||
|
});
|
||||||
|
assert.deepEqual(bounds, { x: 5, width: 20, isActive: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { resolveLineLoadingPulseMode } from "../line-loading-pulse";
|
||||||
|
|
||||||
|
describe("resolveLineLoadingPulseMode", () => {
|
||||||
|
it("maps loading phases to pulse modes", () => {
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("loading"), "loop");
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("exiting"), "exit");
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("revealingLoading"), "enter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for non-loading phases", () => {
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("ready"), null);
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("revealing"), null);
|
||||||
|
assert.equal(resolveLineLoadingPulseMode("exitingReady"), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { getSkeletonHeights } from "../loading-sweep";
|
||||||
|
|
||||||
|
describe("getSkeletonHeights", () => {
|
||||||
|
it("returns the requested number of heights", () => {
|
||||||
|
assert.equal(getSkeletonHeights(12).length, 12);
|
||||||
|
assert.equal(getSkeletonHeights(0).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is deterministic for the same (count, seed) — SSR-safe", () => {
|
||||||
|
assert.deepEqual(getSkeletonHeights(8, 3), getSkeletonHeights(8, 3));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-rolls when the seed changes", () => {
|
||||||
|
assert.notDeepEqual(getSkeletonHeights(8, 0), getSkeletonHeights(8, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays within the default [20, 80) range", () => {
|
||||||
|
for (const h of getSkeletonHeights(50, 7)) {
|
||||||
|
assert.ok(h >= 20 && h < 80, `height ${h} out of default range`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects a custom range", () => {
|
||||||
|
for (const h of getSkeletonHeights(50, 2, 40, 60)) {
|
||||||
|
assert.ok(h >= 40 && h < 60, `height ${h} out of [40, 60)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { splitProfitLossSegments } from "../profit-loss-segments";
|
||||||
|
|
||||||
|
const xAccessor = (d: Record<string, unknown>) => d.date as Date;
|
||||||
|
|
||||||
|
describe("splitProfitLossSegments", () => {
|
||||||
|
it("returns empty array for no data", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
splitProfitLossSegments({
|
||||||
|
data: [],
|
||||||
|
dataKey: "pnl",
|
||||||
|
xAccessor,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits at zero crossings with interpolated crossing points", () => {
|
||||||
|
const data = [
|
||||||
|
{ date: new Date("2024-01-01"), pnl: 100 },
|
||||||
|
{ date: new Date("2024-01-02"), pnl: -50 },
|
||||||
|
{ date: new Date("2024-01-03"), pnl: -20 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const segments = splitProfitLossSegments({
|
||||||
|
data,
|
||||||
|
dataKey: "pnl",
|
||||||
|
xAccessor,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(segments.length, 2);
|
||||||
|
assert.equal(segments[0]?.isPositive, true);
|
||||||
|
assert.equal(segments[1]?.isPositive, false);
|
||||||
|
assert.equal(segments[0]?.data.at(-1)?.pnl, 0);
|
||||||
|
assert.equal(segments[1]?.data[0]?.pnl, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a single segment when values stay on one side of zero", () => {
|
||||||
|
const data = [
|
||||||
|
{ date: new Date("2024-01-01"), pnl: 10 },
|
||||||
|
{ date: new Date("2024-01-02"), pnl: 20 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const segments = splitProfitLossSegments({
|
||||||
|
data,
|
||||||
|
dataKey: "pnl",
|
||||||
|
xAccessor,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(segments.length, 1);
|
||||||
|
assert.equal(segments[0]?.isPositive, true);
|
||||||
|
assert.equal(segments[0]?.data.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
buildHorizontalTangentBezierPath,
|
||||||
|
buildProjectionPath,
|
||||||
|
} from "../projection-utils";
|
||||||
|
|
||||||
|
const sourceData = Array.from({ length: 6 }, (_, index) => ({
|
||||||
|
date: new Date(2024, 0, index + 1),
|
||||||
|
desktop: 100 + index * 10,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("buildProjectionPath", () => {
|
||||||
|
it("extends auto projections from the last point to the horizon", () => {
|
||||||
|
const path = buildProjectionPath({
|
||||||
|
sourceData,
|
||||||
|
seriesKey: "desktop",
|
||||||
|
mode: "auto",
|
||||||
|
autoMethod: "linearRegression",
|
||||||
|
horizonPoints: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(path.length, 2);
|
||||||
|
assert.equal(path[0]?.value, 150);
|
||||||
|
assert.ok((path.at(-1)?.value ?? 0) > 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a target projection to an end value", () => {
|
||||||
|
const path = buildProjectionPath({
|
||||||
|
sourceData,
|
||||||
|
seriesKey: "desktop",
|
||||||
|
mode: "target",
|
||||||
|
horizonPoints: 3,
|
||||||
|
endValue: 220,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(path.length, 2);
|
||||||
|
assert.equal(path[0]?.value, 150);
|
||||||
|
assert.equal(path[1]?.value, 220);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const HORIZONTAL_TANGENT_BEZIER_PATH = /^M 0,100 C 90,100 110,50 200,50$/;
|
||||||
|
|
||||||
|
describe("buildHorizontalTangentBezierPath", () => {
|
||||||
|
it("uses horizontal control points at start and end y", () => {
|
||||||
|
const path = buildHorizontalTangentBezierPath(0, 100, 200, 50);
|
||||||
|
assert.match(path, HORIZONTAL_TANGENT_BEZIER_PATH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
computeReferenceAreaRect,
|
||||||
|
resolveReferenceDataRange,
|
||||||
|
} from "../reference-area-geometry";
|
||||||
|
|
||||||
|
const innerWidth = 400;
|
||||||
|
const innerHeight = 200;
|
||||||
|
|
||||||
|
const xScale = (date: Date) => date.getTime() / 10;
|
||||||
|
const yScale = (value: number) => innerHeight - value;
|
||||||
|
|
||||||
|
function baseOptions(
|
||||||
|
overrides: Partial<Parameters<typeof computeReferenceAreaRect>[0]> = {}
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
innerWidth,
|
||||||
|
innerHeight,
|
||||||
|
xScale,
|
||||||
|
yScale,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("computeReferenceAreaRect", () => {
|
||||||
|
it("maps a full-width horizontal band between y1 and y2", () => {
|
||||||
|
const rect = computeReferenceAreaRect(baseOptions({ y1: 40, y2: 80 }));
|
||||||
|
assert.deepEqual(rect, { x: 0, y: 120, width: 400, height: 40 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a partial x-range when x1 and x2 are set", () => {
|
||||||
|
const rect = computeReferenceAreaRect(
|
||||||
|
baseOptions({
|
||||||
|
x1: new Date(1000),
|
||||||
|
x2: new Date(2000),
|
||||||
|
y1: 50,
|
||||||
|
y2: 100,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.deepEqual(rect, { x: 100, y: 100, width: 100, height: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps to the plot when ifOverflow is hidden", () => {
|
||||||
|
const rect = computeReferenceAreaRect(
|
||||||
|
baseOptions({
|
||||||
|
y1: 150,
|
||||||
|
y2: 250,
|
||||||
|
ifOverflow: "hidden",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.deepEqual(rect, { x: 0, y: 0, width: 400, height: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when discard and partly outside the plot", () => {
|
||||||
|
const rect = computeReferenceAreaRect(
|
||||||
|
baseOptions({
|
||||||
|
y1: 150,
|
||||||
|
y2: 250,
|
||||||
|
ifOverflow: "discard",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.equal(rect, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not clamp when ifOverflow is visible", () => {
|
||||||
|
const rect = computeReferenceAreaRect(
|
||||||
|
baseOptions({
|
||||||
|
y1: 150,
|
||||||
|
y2: 250,
|
||||||
|
ifOverflow: "visible",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert.deepEqual(rect, { x: 0, y: -50, width: 400, height: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for zero plot size", () => {
|
||||||
|
assert.equal(
|
||||||
|
computeReferenceAreaRect(
|
||||||
|
baseOptions({ innerWidth: 0, innerHeight: 200, y1: 10, y2: 20 })
|
||||||
|
),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveReferenceDataRange", () => {
|
||||||
|
it("returns inclusive bounds between y1 and y2", () => {
|
||||||
|
assert.deepEqual(resolveReferenceDataRange(160, 220, [0, 300]), [160, 220]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extends to domain edges when y bounds are omitted", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveReferenceDataRange(undefined, 220, [0, 300]),
|
||||||
|
[0, 220]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveReferenceDataRange(160, undefined, [0, 300]),
|
||||||
|
[160, 300]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
computeSeriesBarRevealClipPadding,
|
||||||
|
computeSeriesBarWidth,
|
||||||
|
} from "../series-bar-layout";
|
||||||
|
|
||||||
|
describe("computeSeriesBarWidth", () => {
|
||||||
|
it("caps grouped bar width to the slot", () => {
|
||||||
|
const width = computeSeriesBarWidth({
|
||||||
|
innerWidth: 400,
|
||||||
|
dataLength: 12,
|
||||||
|
columnWidth: 400 / 11,
|
||||||
|
seriesCount: 2,
|
||||||
|
composedBarGap: 4,
|
||||||
|
});
|
||||||
|
assert.ok(width >= 2);
|
||||||
|
assert.ok(width <= 40);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeSeriesBarRevealClipPadding", () => {
|
||||||
|
it("uses half the bar width for a single stacked column", () => {
|
||||||
|
assert.equal(
|
||||||
|
computeSeriesBarRevealClipPadding({
|
||||||
|
barWidth: 24,
|
||||||
|
seriesCount: 3,
|
||||||
|
stacked: true,
|
||||||
|
}),
|
||||||
|
12
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses half the group width for grouped bars", () => {
|
||||||
|
assert.equal(
|
||||||
|
computeSeriesBarRevealClipPadding({
|
||||||
|
barWidth: 20,
|
||||||
|
seriesCount: 2,
|
||||||
|
gap: 4,
|
||||||
|
}),
|
||||||
|
22
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import {
|
||||||
|
domainsEqual,
|
||||||
|
isReferenceAreaVisiblePhase,
|
||||||
|
mergeYDomainRecords,
|
||||||
|
niceYDomain,
|
||||||
|
shouldTweenYDomain,
|
||||||
|
} from "../y-domain-utils";
|
||||||
|
|
||||||
|
describe("shouldTweenYDomain", () => {
|
||||||
|
it("skips tween when both endpoints move less than 2% of span", () => {
|
||||||
|
assert.equal(shouldTweenYDomain([0, 100], [1, 101]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tweens when min endpoint shifts enough", () => {
|
||||||
|
assert.equal(shouldTweenYDomain([0, 100], [5, 100]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tweens when max endpoint shifts enough", () => {
|
||||||
|
assert.equal(shouldTweenYDomain([0, 100], [0, 110]), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("niceYDomain", () => {
|
||||||
|
it("expands raw domain to nice tick boundaries", () => {
|
||||||
|
const [min, max] = niceYDomain([13, 87]);
|
||||||
|
assert.ok(min <= 13);
|
||||||
|
assert.ok(max >= 87);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mergeYDomainRecords", () => {
|
||||||
|
it("normalizes axis ids and merges maps", () => {
|
||||||
|
const merged = mergeYDomainRecords({ left: [0, 100] }, { right: [10, 50] });
|
||||||
|
assert.deepEqual(merged.left, [0, 100]);
|
||||||
|
assert.deepEqual(merged.right, [10, 50]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("domainsEqual", () => {
|
||||||
|
it("returns true when axis domains match", () => {
|
||||||
|
assert.equal(domainsEqual({ left: [0, 100] }, { left: [0, 100] }), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when any endpoint differs", () => {
|
||||||
|
assert.equal(domainsEqual({ left: [0, 100] }, { left: [0, 110] }), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isReferenceAreaVisiblePhase", () => {
|
||||||
|
it("shows reference areas during ready reveal phases", () => {
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("ready"), true);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("revealing"), true);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("gridTweenReady"), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides reference areas during loading and exit phases", () => {
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("loading"), false);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("exiting"), false);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("exitingReady"), false);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("gridTweenLoading"), false);
|
||||||
|
assert.equal(isReferenceAreaVisiblePhase("revealingLoading"), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/animation.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { Transition } from "motion/react";
|
||||||
|
|
||||||
|
/** Default clip-reveal easing for cartesian charts. */
|
||||||
|
export const DEFAULT_ANIMATION_EASING = "cubic-bezier(0.85, 0, 0.15, 1)";
|
||||||
|
|
||||||
|
export const DEFAULT_ANIMATION_DURATION_MS = 1100;
|
||||||
|
|
||||||
|
/** Default enter transition — matches the original line chart reveal. */
|
||||||
|
export const DEFAULT_CHART_ENTER_TRANSITION: Transition = {
|
||||||
|
type: "tween",
|
||||||
|
duration: DEFAULT_ANIMATION_DURATION_MS / 1000,
|
||||||
|
ease: [0.85, 0, 0.15, 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clip-path width reveal must use tween — spring does not reliably animate SVG width.
|
||||||
|
*/
|
||||||
|
export function clipRevealTransition(enterTransition?: Transition): Transition {
|
||||||
|
if (enterTransition?.type === "tween") {
|
||||||
|
return {
|
||||||
|
...enterTransition,
|
||||||
|
ease: enterTransition.ease ?? DEFAULT_CHART_ENTER_TRANSITION.ease,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration =
|
||||||
|
typeof enterTransition?.duration === "number"
|
||||||
|
? enterTransition.duration
|
||||||
|
: DEFAULT_ANIMATION_DURATION_MS / 1000;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "tween",
|
||||||
|
duration,
|
||||||
|
ease: DEFAULT_CHART_ENTER_TRANSITION.ease,
|
||||||
|
};
|
||||||
|
}
|
||||||
116
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart-loading.tsx
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { curveNatural } from "@visx/curve";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Area } from "./area";
|
||||||
|
import { AreaChart } from "./area-chart";
|
||||||
|
import type { Margin } from "./chart-context";
|
||||||
|
import type { LoadingStyle } from "./chart-phase";
|
||||||
|
import {
|
||||||
|
DEFAULT_SKELETON_DATA_KEY,
|
||||||
|
DEFAULT_SKELETON_POINT_COUNT,
|
||||||
|
generateChartSkeletonData,
|
||||||
|
} from "./generate-chart-skeleton-data";
|
||||||
|
import { Grid } from "./grid";
|
||||||
|
|
||||||
|
const LOADING_DATA_KEY = DEFAULT_SKELETON_DATA_KEY;
|
||||||
|
const DEFAULT_LOADING_STROKE = "var(--foreground)";
|
||||||
|
const DEFAULT_LOADING_GRID_STROKE =
|
||||||
|
"color-mix(in oklch, var(--chart-grid) 50%, transparent)";
|
||||||
|
const DEFAULT_LOADING_GRID_SHIMMER_STROKE =
|
||||||
|
"color-mix(in oklch, var(--foreground) 68%, transparent)";
|
||||||
|
const DEFAULT_LOADING_STROKE_OPACITY = 0.5;
|
||||||
|
|
||||||
|
export interface AreaChartLoadingProps {
|
||||||
|
/** Chart margins */
|
||||||
|
margin?: Partial<Margin>;
|
||||||
|
/** Stroke color for the animated loading segment. */
|
||||||
|
stroke?: string;
|
||||||
|
/** Stroke opacity for the animated loading segment. Default: 0.5 */
|
||||||
|
strokeOpacity?: number;
|
||||||
|
/** Grid line stroke (color and opacity via color-mix or oklch alpha). */
|
||||||
|
gridStroke?: string;
|
||||||
|
/** Shimmer band stroke (color and opacity via color-mix or oklch alpha). */
|
||||||
|
gridShimmerStroke?: string;
|
||||||
|
/** Animate a shimmer band across grid lines. Default: true */
|
||||||
|
gridShimmer?: boolean;
|
||||||
|
/** Shimmer band width in pixels. Default: 140 */
|
||||||
|
gridShimmerLength?: number;
|
||||||
|
/** Shimmer speed multiplier (higher = faster). Default: 1 */
|
||||||
|
gridShimmerSpeed?: number;
|
||||||
|
/** Match shimmer loop to the loading line pulse (cycle + inter-loop pause). */
|
||||||
|
gridShimmerSync?: boolean;
|
||||||
|
/** Loading animation: `"pulse"` (default traveling pulse) or `"sweep"` (a
|
||||||
|
* diagonal shimmer across the skeleton area). Default: `"pulse"`. */
|
||||||
|
loadingStyle?: LoadingStyle;
|
||||||
|
/** Centered shimmer label text. Default: "Loading" */
|
||||||
|
label?: string;
|
||||||
|
/** Aspect ratio as "width / height". Default: "2 / 1" */
|
||||||
|
aspectRatio?: string;
|
||||||
|
/** Additional class name for the container */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AreaChartLoading({
|
||||||
|
margin,
|
||||||
|
stroke = DEFAULT_LOADING_STROKE,
|
||||||
|
strokeOpacity = DEFAULT_LOADING_STROKE_OPACITY,
|
||||||
|
gridStroke = DEFAULT_LOADING_GRID_STROKE,
|
||||||
|
gridShimmerStroke = DEFAULT_LOADING_GRID_SHIMMER_STROKE,
|
||||||
|
gridShimmer = true,
|
||||||
|
gridShimmerLength,
|
||||||
|
gridShimmerSpeed,
|
||||||
|
gridShimmerSync = false,
|
||||||
|
loadingStyle = "pulse",
|
||||||
|
label = "Loading",
|
||||||
|
aspectRatio = "2 / 1",
|
||||||
|
className = "",
|
||||||
|
}: AreaChartLoadingProps) {
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
generateChartSkeletonData({
|
||||||
|
dataKey: DEFAULT_SKELETON_DATA_KEY,
|
||||||
|
pointCount: DEFAULT_SKELETON_POINT_COUNT,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AreaChart
|
||||||
|
animationDuration={0}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
className={className}
|
||||||
|
data={data}
|
||||||
|
loadingLabel={label}
|
||||||
|
margin={margin}
|
||||||
|
status="loading"
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
horizontal
|
||||||
|
shimmer={loadingStyle === "sweep" ? false : gridShimmer}
|
||||||
|
shimmerLength={gridShimmerLength}
|
||||||
|
shimmerSpeed={gridShimmerSpeed}
|
||||||
|
shimmerStroke={gridShimmerStroke}
|
||||||
|
shimmerSync={gridShimmerSync}
|
||||||
|
stroke={gridStroke}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
curve={curveNatural}
|
||||||
|
dataKey={LOADING_DATA_KEY}
|
||||||
|
fadeEdges={false}
|
||||||
|
fill="transparent"
|
||||||
|
fillOpacity={0}
|
||||||
|
loading
|
||||||
|
loadingStroke={stroke}
|
||||||
|
loadingStrokeOpacity={strokeOpacity}
|
||||||
|
loadingStyle={loadingStyle}
|
||||||
|
showHighlight={false}
|
||||||
|
showLine
|
||||||
|
stroke="transparent"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AreaChartLoading;
|
||||||
272
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-chart.tsx
vendored
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ParentSize } from "@visx/responsive";
|
||||||
|
import type { Transition } from "motion/react";
|
||||||
|
import {
|
||||||
|
Children,
|
||||||
|
type CSSProperties,
|
||||||
|
isValidElement,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Area, type AreaProps } from "./area";
|
||||||
|
import type { LineConfig, Margin } from "./chart-context";
|
||||||
|
import { ChartLoadingLabel } from "./chart-loading-label";
|
||||||
|
import {
|
||||||
|
type ChartPhase,
|
||||||
|
type ChartStatus,
|
||||||
|
DEFAULT_CHART_STATUS,
|
||||||
|
DEFAULT_Y_DOMAIN_TWEEN_MS,
|
||||||
|
resolveRestingChartPhase,
|
||||||
|
} from "./chart-phase";
|
||||||
|
import { PatternArea } from "./pattern-area";
|
||||||
|
import { TimeSeriesChartInner } from "./time-series-chart-shell";
|
||||||
|
|
||||||
|
export interface AreaChartProps {
|
||||||
|
/** Data array - each item should have a date field and numeric values */
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
/** Key in data for the x-axis (date). Default: "date" */
|
||||||
|
xDataKey?: string;
|
||||||
|
/** Chart margins */
|
||||||
|
margin?: Partial<Margin>;
|
||||||
|
/** Animation duration in milliseconds. Default: 1100 */
|
||||||
|
animationDuration?: number;
|
||||||
|
/** CSS easing for clip-reveal. Default: cubic-bezier(0.85, 0, 0.15, 1) */
|
||||||
|
animationEasing?: string;
|
||||||
|
/** Motion enter transition (spring or cubic-bezier tween). */
|
||||||
|
enterTransition?: Transition;
|
||||||
|
/** Signature of motion URL state — triggers reveal replay when it changes. */
|
||||||
|
revealSignature?: string;
|
||||||
|
/** Aspect ratio as "width / height". Default: "2 / 1" */
|
||||||
|
aspectRatio?: string;
|
||||||
|
/** Additional class name for the container */
|
||||||
|
className?: string;
|
||||||
|
/** Loading vs ready — drives chart phase and loading chrome. Default: `"ready"`. */
|
||||||
|
status?: ChartStatus;
|
||||||
|
/** Centered shimmer label while loading. */
|
||||||
|
loadingLabel?: string;
|
||||||
|
/** Animate y-domain over this duration (ms) on status transitions. Default: 500. */
|
||||||
|
yDomainTweenDuration?: number;
|
||||||
|
/** Animate y-domain when status or target domain changes. Default: true */
|
||||||
|
yDomainTween?: boolean;
|
||||||
|
/** Visible x-domain for brush zoom. */
|
||||||
|
xDomain?: [Date, Date];
|
||||||
|
/** Full dataset length for x-scale padding when `xDomain` is set. */
|
||||||
|
xDomainSlotCount?: number;
|
||||||
|
/** Tween y-domain when brush changes the visible x-range. Default: false */
|
||||||
|
tweenYDomainOnXDomainChange?: boolean;
|
||||||
|
/** Inline container styles (e.g. fixed height for brush strip). */
|
||||||
|
style?: CSSProperties;
|
||||||
|
/** Fires when the internal chart phase changes (e.g. OG capture readiness). */
|
||||||
|
onPhaseChange?: (phase: ChartPhase) => void;
|
||||||
|
/** Child components (Area, Grid, ChartTooltip, etc.) */
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };
|
||||||
|
|
||||||
|
function extractAreaConfigs(children: ReactNode): LineConfig[] {
|
||||||
|
const configs: LineConfig[] = [];
|
||||||
|
|
||||||
|
Children.forEach(children, (child) => {
|
||||||
|
if (!isValidElement(child)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childType = child.type as {
|
||||||
|
displayName?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
const componentName =
|
||||||
|
typeof child.type === "function"
|
||||||
|
? childType.displayName || childType.name || ""
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const props = child.props as AreaProps | undefined;
|
||||||
|
const isPatternArea =
|
||||||
|
componentName === "PatternArea" || child.type === PatternArea;
|
||||||
|
const isAreaComponent =
|
||||||
|
componentName === "Area" ||
|
||||||
|
child.type === Area ||
|
||||||
|
(props &&
|
||||||
|
typeof props.dataKey === "string" &&
|
||||||
|
props.dataKey.length > 0 &&
|
||||||
|
!isPatternArea);
|
||||||
|
|
||||||
|
if (isAreaComponent && props?.dataKey) {
|
||||||
|
configs.push({
|
||||||
|
dataKey: props.dataKey,
|
||||||
|
stroke: props.stroke || props.fill || "var(--chart-line-primary)",
|
||||||
|
strokeWidth: props.strokeWidth || 2,
|
||||||
|
yAxisId: props.yAxisId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartInnerProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
data: Record<string, unknown>[];
|
||||||
|
xDataKey: string;
|
||||||
|
margin: Margin;
|
||||||
|
animationDuration: number;
|
||||||
|
animationEasing?: string;
|
||||||
|
enterTransition?: Transition;
|
||||||
|
revealSignature?: string;
|
||||||
|
chartStatus: ChartStatus;
|
||||||
|
loadingLabel?: string;
|
||||||
|
yDomainTweenDuration: number;
|
||||||
|
yDomainTween: boolean;
|
||||||
|
xDomain?: [Date, Date];
|
||||||
|
xDomainSlotCount?: number;
|
||||||
|
tweenYDomainOnXDomainChange?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
onPhaseChange: (phase: ChartPhase) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartInner({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data,
|
||||||
|
xDataKey,
|
||||||
|
margin,
|
||||||
|
animationDuration,
|
||||||
|
animationEasing,
|
||||||
|
enterTransition,
|
||||||
|
revealSignature,
|
||||||
|
chartStatus,
|
||||||
|
loadingLabel,
|
||||||
|
yDomainTweenDuration,
|
||||||
|
yDomainTween,
|
||||||
|
xDomain,
|
||||||
|
xDomainSlotCount,
|
||||||
|
tweenYDomainOnXDomainChange,
|
||||||
|
children,
|
||||||
|
containerRef,
|
||||||
|
onPhaseChange,
|
||||||
|
}: ChartInnerProps) {
|
||||||
|
const lines = useMemo(() => extractAreaConfigs(children), [children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimeSeriesChartInner
|
||||||
|
animationDuration={animationDuration}
|
||||||
|
animationEasing={animationEasing}
|
||||||
|
chartStatus={chartStatus}
|
||||||
|
clipPathId="chart-area-grow-clip"
|
||||||
|
containerRef={containerRef}
|
||||||
|
data={data}
|
||||||
|
enterTransition={enterTransition}
|
||||||
|
height={height}
|
||||||
|
lines={lines}
|
||||||
|
loadingLabel={loadingLabel}
|
||||||
|
margin={margin}
|
||||||
|
onPhaseChange={onPhaseChange}
|
||||||
|
revealSignature={revealSignature}
|
||||||
|
tweenYDomainOnXDomainChange={tweenYDomainOnXDomainChange}
|
||||||
|
width={width}
|
||||||
|
xDataKey={xDataKey}
|
||||||
|
xDomain={xDomain}
|
||||||
|
xDomainSlotCount={xDomainSlotCount}
|
||||||
|
yDomainTween={yDomainTween}
|
||||||
|
yDomainTweenDuration={yDomainTweenDuration}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TimeSeriesChartInner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AreaChart({
|
||||||
|
data,
|
||||||
|
xDataKey = "date",
|
||||||
|
margin: marginProp,
|
||||||
|
animationDuration = 1100,
|
||||||
|
animationEasing,
|
||||||
|
enterTransition,
|
||||||
|
revealSignature,
|
||||||
|
aspectRatio = "2 / 1",
|
||||||
|
className = "",
|
||||||
|
status = DEFAULT_CHART_STATUS,
|
||||||
|
loadingLabel,
|
||||||
|
yDomainTweenDuration = DEFAULT_Y_DOMAIN_TWEEN_MS,
|
||||||
|
yDomainTween = true,
|
||||||
|
xDomain,
|
||||||
|
xDomainSlotCount,
|
||||||
|
tweenYDomainOnXDomainChange = false,
|
||||||
|
style,
|
||||||
|
onPhaseChange,
|
||||||
|
children,
|
||||||
|
}: AreaChartProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const margin = { ...DEFAULT_MARGIN, ...marginProp };
|
||||||
|
const [chartPhase, setChartPhase] = useState<ChartPhase>(() =>
|
||||||
|
resolveRestingChartPhase(status)
|
||||||
|
);
|
||||||
|
const handlePhaseChange = useCallback(
|
||||||
|
(phase: ChartPhase) => {
|
||||||
|
setChartPhase(phase);
|
||||||
|
onPhaseChange?.(phase);
|
||||||
|
},
|
||||||
|
[onPhaseChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showLoadingLabel = Boolean(
|
||||||
|
loadingLabel?.trim() &&
|
||||||
|
(chartPhase === "loading" ||
|
||||||
|
chartPhase === "exiting" ||
|
||||||
|
chartPhase === "gridTweenReady" ||
|
||||||
|
chartPhase === "revealingLoading")
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("relative w-full", className)}
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ aspectRatio, touchAction: "none", ...style }}
|
||||||
|
>
|
||||||
|
<ParentSize debounceTime={10}>
|
||||||
|
{({ width, height }) => (
|
||||||
|
<ChartInner
|
||||||
|
animationDuration={animationDuration}
|
||||||
|
animationEasing={animationEasing}
|
||||||
|
chartStatus={status}
|
||||||
|
containerRef={containerRef}
|
||||||
|
data={data}
|
||||||
|
enterTransition={enterTransition}
|
||||||
|
height={height}
|
||||||
|
loadingLabel={loadingLabel}
|
||||||
|
margin={margin}
|
||||||
|
onPhaseChange={handlePhaseChange}
|
||||||
|
revealSignature={revealSignature}
|
||||||
|
tweenYDomainOnXDomainChange={tweenYDomainOnXDomainChange}
|
||||||
|
width={width}
|
||||||
|
xDataKey={xDataKey}
|
||||||
|
xDomain={xDomain}
|
||||||
|
xDomainSlotCount={xDomainSlotCount}
|
||||||
|
yDomainTween={yDomainTween}
|
||||||
|
yDomainTweenDuration={yDomainTweenDuration}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ChartInner>
|
||||||
|
)}
|
||||||
|
</ParentSize>
|
||||||
|
{showLoadingLabel ? (
|
||||||
|
<ChartLoadingLabel
|
||||||
|
exiting={chartPhase !== "loading"}
|
||||||
|
text={loadingLabel}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Area, type AreaProps } from "./area";
|
||||||
|
|
||||||
|
export default AreaChart;
|
||||||
113
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/area-gradient-defs.tsx
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import {
|
||||||
|
type FadeEdges,
|
||||||
|
fadeGradientStops,
|
||||||
|
resolveFadeSides,
|
||||||
|
viewportFadeGradientAttrs,
|
||||||
|
} from "./fade-edges";
|
||||||
|
|
||||||
|
interface AreaGradientDefsProps {
|
||||||
|
gradientId: string;
|
||||||
|
strokeGradientId: string;
|
||||||
|
edgeMaskId: string;
|
||||||
|
edgeGradientId: string;
|
||||||
|
fill: string;
|
||||||
|
fillOpacity: number;
|
||||||
|
gradientToOpacity: number;
|
||||||
|
/** 0–1: where the bottom stop sits (1 = full-height gradient). */
|
||||||
|
gradientSpan?: number;
|
||||||
|
resolvedStroke: string;
|
||||||
|
isPatternFill: boolean;
|
||||||
|
fadeEdges: FadeEdges;
|
||||||
|
innerWidth: number;
|
||||||
|
innerHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AreaGradientDefs({
|
||||||
|
gradientId,
|
||||||
|
strokeGradientId,
|
||||||
|
edgeMaskId,
|
||||||
|
edgeGradientId,
|
||||||
|
fill,
|
||||||
|
fillOpacity,
|
||||||
|
gradientToOpacity,
|
||||||
|
gradientSpan = 1,
|
||||||
|
resolvedStroke,
|
||||||
|
isPatternFill,
|
||||||
|
fadeEdges,
|
||||||
|
innerWidth,
|
||||||
|
innerHeight,
|
||||||
|
}: AreaGradientDefsProps) {
|
||||||
|
const sides = resolveFadeSides(fadeEdges);
|
||||||
|
// Stroke gradient mirrors the area's edge fade so the line doesn't pop in
|
||||||
|
// past the faded fill. Skip emitting it when neither edge fades — the line
|
||||||
|
// can then paint a solid stroke instead of an unnecessary url(#...) ref.
|
||||||
|
const strokeStops = sides.any ? fadeGradientStops(sides) : null;
|
||||||
|
const showEdgeMask = sides.any && !isPatternFill;
|
||||||
|
const edgeStops = showEdgeMask ? fadeGradientStops(sides) : null;
|
||||||
|
const span = Math.min(1, Math.max(0.01, gradientSpan));
|
||||||
|
const midOffset = `${span * 100}%`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<defs>
|
||||||
|
{isPatternFill ? null : (
|
||||||
|
<linearGradient id={gradientId} x1="0%" x2="0%" y1="0%" y2="100%">
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
style={{ stopColor: fill, stopOpacity: fillOpacity }}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset={midOffset}
|
||||||
|
style={{ stopColor: fill, stopOpacity: gradientToOpacity }}
|
||||||
|
/>
|
||||||
|
{span < 1 ? (
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style={{ stopColor: fill, stopOpacity: gradientToOpacity }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</linearGradient>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{strokeStops ? (
|
||||||
|
<linearGradient
|
||||||
|
id={strokeGradientId}
|
||||||
|
{...viewportFadeGradientAttrs(innerWidth)}
|
||||||
|
>
|
||||||
|
{strokeStops.map((stop) => (
|
||||||
|
<stop
|
||||||
|
key={stop.offset}
|
||||||
|
offset={stop.offset}
|
||||||
|
style={{ stopColor: resolvedStroke, stopOpacity: stop.opacity }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</linearGradient>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{edgeStops ? (
|
||||||
|
<>
|
||||||
|
<linearGradient
|
||||||
|
id={edgeGradientId}
|
||||||
|
{...viewportFadeGradientAttrs(innerWidth)}
|
||||||
|
>
|
||||||
|
{edgeStops.map((stop) => (
|
||||||
|
<stop
|
||||||
|
key={stop.offset}
|
||||||
|
offset={stop.offset}
|
||||||
|
style={{ stopColor: "white", stopOpacity: stop.opacity }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</linearGradient>
|
||||||
|
<mask id={edgeMaskId}>
|
||||||
|
<rect
|
||||||
|
fill={`url(#${edgeGradientId})`}
|
||||||
|
height={innerHeight}
|
||||||
|
width={innerWidth}
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</defs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,377 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { curveMonotoneX } from "@visx/curve";
|
||||||
|
import { AreaClosed, LinePath } from "@visx/shape";
|
||||||
|
|
||||||
|
// CurveFactory type - simplified version compatible with visx
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type
|
||||||
|
type CurveFactory = any;
|
||||||
|
|
||||||
|
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
||||||
|
import { AreaGradientDefs } from "./area-gradient-defs";
|
||||||
|
import { chartCssVars, useChartStable, useYScale } from "./chart-context";
|
||||||
|
import type { ChartPhase, LoadingStyle } from "./chart-phase";
|
||||||
|
import { type FadeEdges, resolveFadeSides } from "./fade-edges";
|
||||||
|
import {
|
||||||
|
type LineLoadingPulseMode,
|
||||||
|
LineLoadingPulseStroke,
|
||||||
|
resolveLineLoadingPulseMode,
|
||||||
|
} from "./line-loading-pulse";
|
||||||
|
import { LINE_LOADING_LOOP_PAUSE_MS } from "./line-loading-timing";
|
||||||
|
import { LineLoadingSweep } from "./loading-sweep";
|
||||||
|
import {
|
||||||
|
resolveDashTailBounds,
|
||||||
|
usePathStrokeMetrics,
|
||||||
|
} from "./path-stroke-utils";
|
||||||
|
import { SeriesDashTailOverlay } from "./series-dash-tail-overlay";
|
||||||
|
import { SeriesHighlightLayer } from "./series-highlight-layer";
|
||||||
|
import { SeriesHoverDim } from "./series-hover-dim";
|
||||||
|
import { SeriesMarkers } from "./series-markers";
|
||||||
|
import type { SeriesPointMarkerStyle } from "./series-point-marker";
|
||||||
|
|
||||||
|
export interface AreaProps {
|
||||||
|
/** Key in data to use for y values */
|
||||||
|
dataKey: string;
|
||||||
|
/** Y-scale group id (Recharts `yAxisId`). Default: `"left"`. */
|
||||||
|
yAxisId?: string | number;
|
||||||
|
/** Fill color for the area gradient start. Default: var(--chart-line-primary) */
|
||||||
|
fill?: string;
|
||||||
|
/** Fill opacity at the top of the area. Default: 0.4 */
|
||||||
|
fillOpacity?: number;
|
||||||
|
/** Stroke color for the line. Default: same as fill */
|
||||||
|
stroke?: string;
|
||||||
|
/** Stroke width. Default: 2 */
|
||||||
|
strokeWidth?: number;
|
||||||
|
/** Curve function. Default: curveMonotoneX */
|
||||||
|
curve?: CurveFactory;
|
||||||
|
/** Whether to animate the area. Default: true */
|
||||||
|
animate?: boolean;
|
||||||
|
/** Whether to show the stroke line. Default: true */
|
||||||
|
showLine?: boolean;
|
||||||
|
/** Whether to show highlight segment on hover. Default: true */
|
||||||
|
showHighlight?: boolean;
|
||||||
|
/** Gradient opacity at bottom (0 = fully transparent). Default: 0 */
|
||||||
|
gradientToOpacity?: number;
|
||||||
|
/**
|
||||||
|
* Vertical extent of the fill gradient (0–1). `1` fades across the full
|
||||||
|
* height; lower values compress the gradient toward the top.
|
||||||
|
*/
|
||||||
|
gradientSpan?: number;
|
||||||
|
/**
|
||||||
|
* Fade the area fill (and stroke) toward transparent at the chart edges.
|
||||||
|
* - `true` fades both edges, `false` disables the fade entirely.
|
||||||
|
* - `"left"` / `"right"` fades only that side — useful when the opposite
|
||||||
|
* edge butts up against another element you don't want to fade into.
|
||||||
|
* Default: false
|
||||||
|
*/
|
||||||
|
fadeEdges?: FadeEdges;
|
||||||
|
/** Render scatter-style circle markers at each data point. Default: false */
|
||||||
|
showMarkers?: boolean;
|
||||||
|
/** Marker styling (same options as Scatter). */
|
||||||
|
markers?: SeriesPointMarkerStyle;
|
||||||
|
/**
|
||||||
|
* Data index from which the line stroke becomes dashed (inclusive).
|
||||||
|
* Useful for projecting incomplete periods, e.g. dashed from yesterday through today.
|
||||||
|
*/
|
||||||
|
dashFromIndex?: number;
|
||||||
|
/** Dash pattern for the tail segment when `dashFromIndex` is set. Default: "6,4" */
|
||||||
|
dashArray?: string;
|
||||||
|
/** Pulse stroke color while chart is loading. Default: var(--foreground) */
|
||||||
|
loadingStroke?: string;
|
||||||
|
/** Pulse stroke opacity while chart is loading. Default: 0.5 */
|
||||||
|
loadingStrokeOpacity?: number;
|
||||||
|
/**
|
||||||
|
* Show the loading pulse overlay. Default: follows chart loading phase.
|
||||||
|
* Set `false` to disable even during loading.
|
||||||
|
*/
|
||||||
|
loading?: boolean;
|
||||||
|
/** Override pulse animation mode (loop / exit / enter). */
|
||||||
|
loadingPulseMode?: LineLoadingPulseMode;
|
||||||
|
/**
|
||||||
|
* Loading animation while the chart is in loading status: the default
|
||||||
|
* traveling `"pulse"`, or a diagonal `"sweep"` shimmer across the skeleton
|
||||||
|
* area. Default: `"pulse"`.
|
||||||
|
*/
|
||||||
|
loadingStyle?: LoadingStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAreaLoadingPulseState(
|
||||||
|
chartPhase: ChartPhase,
|
||||||
|
loading: boolean | undefined,
|
||||||
|
loadingPulseMode: LineLoadingPulseMode | undefined,
|
||||||
|
notifyLoadingPulseComplete?: () => void
|
||||||
|
) {
|
||||||
|
const phasePulseMode = resolveLineLoadingPulseMode(chartPhase);
|
||||||
|
const pulseMode =
|
||||||
|
loading === false
|
||||||
|
? null
|
||||||
|
: (loadingPulseMode ?? (loading === true ? "loop" : phasePulseMode));
|
||||||
|
const showLoadingPulse = pulseMode != null;
|
||||||
|
const showSeriesContent =
|
||||||
|
chartPhase === "revealing" ||
|
||||||
|
chartPhase === "ready" ||
|
||||||
|
chartPhase === "exitingReady";
|
||||||
|
const [pulseEpoch, setPulseEpoch] = useState(0);
|
||||||
|
|
||||||
|
const handleLoadingPulseComplete = useCallback(() => {
|
||||||
|
if (pulseMode === "loop") {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setPulseEpoch((epoch) => epoch + 1);
|
||||||
|
}, LINE_LOADING_LOOP_PAUSE_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyLoadingPulseComplete?.();
|
||||||
|
}, [notifyLoadingPulseComplete, pulseMode]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleLoadingPulseComplete,
|
||||||
|
pulseMode,
|
||||||
|
pulseEpoch,
|
||||||
|
showLoadingPulse,
|
||||||
|
showSeriesContent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors Line series layout (fill, stroke, dash, markers, pulse)
|
||||||
|
export function Area({
|
||||||
|
dataKey,
|
||||||
|
yAxisId,
|
||||||
|
fill = chartCssVars.linePrimary,
|
||||||
|
fillOpacity = 0.4,
|
||||||
|
stroke,
|
||||||
|
strokeWidth = 2,
|
||||||
|
curve = curveMonotoneX,
|
||||||
|
animate = true,
|
||||||
|
showLine = true,
|
||||||
|
showHighlight = true,
|
||||||
|
gradientToOpacity = 0,
|
||||||
|
gradientSpan = 1,
|
||||||
|
fadeEdges = false,
|
||||||
|
showMarkers = false,
|
||||||
|
markers,
|
||||||
|
dashFromIndex,
|
||||||
|
dashArray = "6,4",
|
||||||
|
loading,
|
||||||
|
loadingStroke = chartCssVars.foreground,
|
||||||
|
loadingStrokeOpacity = 0.5,
|
||||||
|
loadingPulseMode,
|
||||||
|
loadingStyle = "pulse",
|
||||||
|
}: AreaProps) {
|
||||||
|
// Stable slice only: hover state lives inside `<SeriesHoverDim>` and
|
||||||
|
// `<SeriesHighlightLayer>` so this component (and its expensive
|
||||||
|
// <SeriesDashTailOverlay> child) does not re-render on cursor motion.
|
||||||
|
// The reveal-clip is now a single shared clipPath at the chart-shell
|
||||||
|
// level (`time-series-chart-shell.tsx`); we no longer render a per-area
|
||||||
|
// `<ChartRevealClip>` or read `revealEpoch` here.
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
renderData,
|
||||||
|
xScale,
|
||||||
|
innerHeight,
|
||||||
|
innerWidth,
|
||||||
|
xAccessor,
|
||||||
|
lines,
|
||||||
|
chartPhase,
|
||||||
|
notifyLoadingPulseComplete,
|
||||||
|
} = useChartStable();
|
||||||
|
const yScale = useYScale(yAxisId);
|
||||||
|
const {
|
||||||
|
handleLoadingPulseComplete,
|
||||||
|
pulseMode,
|
||||||
|
pulseEpoch,
|
||||||
|
showLoadingPulse,
|
||||||
|
showSeriesContent,
|
||||||
|
} = useAreaLoadingPulseState(
|
||||||
|
chartPhase,
|
||||||
|
loading,
|
||||||
|
loadingPulseMode,
|
||||||
|
notifyLoadingPulseComplete
|
||||||
|
);
|
||||||
|
|
||||||
|
const seriesIndex = useMemo(() => {
|
||||||
|
const index = lines.findIndex((line) => line.dataKey === dataKey);
|
||||||
|
return index >= 0 ? index : 0;
|
||||||
|
}, [lines, dataKey]);
|
||||||
|
|
||||||
|
const pathRef = useRef<SVGPathElement>(null);
|
||||||
|
const { pathLength, pathD } = usePathStrokeMetrics(pathRef, [
|
||||||
|
renderData,
|
||||||
|
innerWidth,
|
||||||
|
dashFromIndex,
|
||||||
|
showLine,
|
||||||
|
showSeriesContent,
|
||||||
|
showLoadingPulse,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Unique IDs for this area
|
||||||
|
const uniqueId = useId();
|
||||||
|
const gradientId = `area-gradient-${dataKey}-${uniqueId}`;
|
||||||
|
const strokeGradientId = `area-stroke-gradient-${dataKey}-${uniqueId}`;
|
||||||
|
const edgeMaskId = `area-edge-mask-${dataKey}-${uniqueId}`;
|
||||||
|
const edgeGradientId = `${edgeMaskId}-gradient`;
|
||||||
|
|
||||||
|
const isPatternFill = fill.startsWith("url(");
|
||||||
|
const showAreaFill = isPatternFill || fillOpacity > 0;
|
||||||
|
const areaFill = isPatternFill ? fill : `url(#${gradientId})`;
|
||||||
|
|
||||||
|
// Resolved stroke color (defaults to fill; pattern URLs need a real color)
|
||||||
|
const resolvedStroke =
|
||||||
|
stroke || (isPatternFill ? chartCssVars.linePrimary : fill);
|
||||||
|
|
||||||
|
const getY = useCallback(
|
||||||
|
(d: Record<string, unknown>) => {
|
||||||
|
const value = d[dataKey];
|
||||||
|
return typeof value === "number" ? (yScale(value) ?? 0) : 0;
|
||||||
|
},
|
||||||
|
[dataKey, yScale]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasDashTail = resolveDashTailBounds(dashFromIndex, data.length);
|
||||||
|
// The stroke gradient is only emitted when at least one edge fades, so fall
|
||||||
|
// back to the resolved solid color otherwise — avoids an invalid url(#...).
|
||||||
|
const fadeSides = resolveFadeSides(fadeEdges);
|
||||||
|
const useViewportEdgeFade = fadeSides.any && !isPatternFill;
|
||||||
|
let strokePaint = resolvedStroke;
|
||||||
|
if (!useViewportEdgeFade && fadeSides.any) {
|
||||||
|
strokePaint = `url(#${strokeGradientId})`;
|
||||||
|
}
|
||||||
|
const highlightEnabled =
|
||||||
|
showHighlight && showLine && !showLoadingPulse && showSeriesContent;
|
||||||
|
const showSeriesStroke = showSeriesContent && showLine;
|
||||||
|
let visibleStroke = "transparent";
|
||||||
|
if (showSeriesStroke && !hasDashTail) {
|
||||||
|
visibleStroke = strokePaint;
|
||||||
|
}
|
||||||
|
const shouldMeasurePath = showLine && (showSeriesContent || showLoadingPulse);
|
||||||
|
|
||||||
|
const seriesLayers = (
|
||||||
|
<>
|
||||||
|
{showSeriesContent && showAreaFill ? (
|
||||||
|
<AreaClosed
|
||||||
|
curve={curve}
|
||||||
|
data={renderData}
|
||||||
|
fill={areaFill}
|
||||||
|
x={(d) => xScale(xAccessor(d)) ?? 0}
|
||||||
|
y={getY}
|
||||||
|
yScale={yScale}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{shouldMeasurePath ? (
|
||||||
|
<>
|
||||||
|
<LinePath
|
||||||
|
curve={curve}
|
||||||
|
data={renderData}
|
||||||
|
innerRef={pathRef}
|
||||||
|
stroke={visibleStroke}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
x={(d) => xScale(xAccessor(d)) ?? 0}
|
||||||
|
y={getY}
|
||||||
|
/>
|
||||||
|
{showSeriesStroke ? (
|
||||||
|
<SeriesDashTailOverlay
|
||||||
|
dashArray={dashArray}
|
||||||
|
dashFromIndex={dashFromIndex}
|
||||||
|
data={data}
|
||||||
|
innerHeight={innerHeight}
|
||||||
|
innerWidth={innerWidth}
|
||||||
|
pathD={pathD}
|
||||||
|
pathLength={pathLength}
|
||||||
|
stroke={strokePaint}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
xAccessor={xAccessor}
|
||||||
|
xScale={xScale}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sweep style owns all loading modes (loop + the exit/enter transitions),
|
||||||
|
// drawing its own silhouette; the pulse covers the default style.
|
||||||
|
const sweepLoading =
|
||||||
|
showLoadingPulse && innerWidth > 0 && loadingStyle === "sweep";
|
||||||
|
const pulseLoading = showLoadingPulse && innerWidth > 0 && !sweepLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AreaGradientDefs
|
||||||
|
edgeGradientId={edgeGradientId}
|
||||||
|
edgeMaskId={edgeMaskId}
|
||||||
|
fadeEdges={fadeEdges}
|
||||||
|
fill={fill}
|
||||||
|
fillOpacity={fillOpacity}
|
||||||
|
gradientId={gradientId}
|
||||||
|
gradientSpan={gradientSpan}
|
||||||
|
gradientToOpacity={gradientToOpacity}
|
||||||
|
innerHeight={innerHeight}
|
||||||
|
innerWidth={innerWidth}
|
||||||
|
isPatternFill={isPatternFill}
|
||||||
|
resolvedStroke={resolvedStroke}
|
||||||
|
strokeGradientId={strokeGradientId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SeriesHoverDim
|
||||||
|
dimOpacity={0.6}
|
||||||
|
enabled={showHighlight}
|
||||||
|
seriesIndex={seriesIndex}
|
||||||
|
>
|
||||||
|
{useViewportEdgeFade ? (
|
||||||
|
<g mask={`url(#${edgeMaskId})`}>{seriesLayers}</g>
|
||||||
|
) : (
|
||||||
|
seriesLayers
|
||||||
|
)}
|
||||||
|
</SeriesHoverDim>
|
||||||
|
|
||||||
|
{/* Highlight segment on hover — isolated hover subscriber. */}
|
||||||
|
<SeriesHighlightLayer
|
||||||
|
enabled={highlightEnabled}
|
||||||
|
height={innerHeight}
|
||||||
|
pathRef={pathRef}
|
||||||
|
stroke={resolvedStroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showMarkers && showSeriesContent ? (
|
||||||
|
<SeriesMarkers
|
||||||
|
animate={animate}
|
||||||
|
dataKey={dataKey}
|
||||||
|
{...markers}
|
||||||
|
fill={markers?.fill ?? resolvedStroke}
|
||||||
|
stroke={markers?.stroke ?? markers?.fill ?? resolvedStroke}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{sweepLoading ? (
|
||||||
|
<LineLoadingSweep
|
||||||
|
curve={curve}
|
||||||
|
key="loading-sweep"
|
||||||
|
mode={pulseMode ?? "loop"}
|
||||||
|
onTransitionComplete={handleLoadingPulseComplete}
|
||||||
|
stroke={loadingStroke}
|
||||||
|
strokeOpacity={loadingStrokeOpacity}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
withArea
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{pulseLoading && pathD ? (
|
||||||
|
<LineLoadingPulseStroke
|
||||||
|
key="loading-pulse"
|
||||||
|
loopEpoch={pulseEpoch}
|
||||||
|
mode={pulseMode ?? undefined}
|
||||||
|
onCycleComplete={handleLoadingPulseComplete}
|
||||||
|
pathD={pathD}
|
||||||
|
stroke={loadingStroke}
|
||||||
|
strokeOpacity={loadingStrokeOpacity}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Area.displayName = "Area";
|
||||||
|
|
||||||
|
export default Area;
|
||||||
219
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/background.tsx
vendored
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { useId, useMemo } from "react";
|
||||||
|
import { useChartStable } from "./chart-context";
|
||||||
|
import {
|
||||||
|
type PatternPresetId,
|
||||||
|
type PatternPresetOptions,
|
||||||
|
renderPatternPreset,
|
||||||
|
} from "./pattern-preset";
|
||||||
|
|
||||||
|
export type BackgroundPatternPreset = PatternPresetId;
|
||||||
|
|
||||||
|
export interface BackgroundProps extends PatternPresetOptions {
|
||||||
|
/** Pattern preset. `"none"` renders nothing. */
|
||||||
|
pattern?: BackgroundPatternPreset;
|
||||||
|
/** Pattern stroke color. Default: `var(--chart-grid)` */
|
||||||
|
color?: string;
|
||||||
|
/** Apply the pattern texture to the plot area. Default: true */
|
||||||
|
showFill?: boolean;
|
||||||
|
/** Pattern fill opacity. Default: 1 */
|
||||||
|
opacity?: number;
|
||||||
|
/** Fade pattern at the left and right chart edges. Default: true */
|
||||||
|
fadeHorizontal?: boolean;
|
||||||
|
/** Fade pattern at the top and bottom chart edges. Default: true */
|
||||||
|
fadeVertical?: boolean;
|
||||||
|
/** Horizontal fade zone as % of plot width per edge. Default: 10 */
|
||||||
|
fadeHorizontalLength?: number;
|
||||||
|
/** Vertical fade zone as % of plot height per edge. Default: 10 */
|
||||||
|
fadeVerticalLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKGROUND_ENTER_FADE_MS = 420;
|
||||||
|
|
||||||
|
function clampFadeLength(length: number): number {
|
||||||
|
return Math.min(45, Math.max(0, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fadeMaskStops(lengthPercent: number): Array<{
|
||||||
|
offset: string;
|
||||||
|
opacity: number;
|
||||||
|
}> {
|
||||||
|
const edge = clampFadeLength(lengthPercent);
|
||||||
|
return [
|
||||||
|
{ offset: "0%", opacity: 0 },
|
||||||
|
{ offset: `${edge}%`, opacity: 1 },
|
||||||
|
{ offset: `${100 - edge}%`, opacity: 1 },
|
||||||
|
{ offset: "100%", opacity: 0 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plot-area pattern fill for charts without a grid. Renders behind series layers. */
|
||||||
|
export function Background({
|
||||||
|
pattern = "diagonal",
|
||||||
|
color = "var(--chart-grid)",
|
||||||
|
scale = 1,
|
||||||
|
strokeWidth,
|
||||||
|
radius,
|
||||||
|
complement,
|
||||||
|
fill,
|
||||||
|
dotFill,
|
||||||
|
tileBackground,
|
||||||
|
showFill = true,
|
||||||
|
opacity = 1,
|
||||||
|
fadeHorizontal = true,
|
||||||
|
fadeVertical = true,
|
||||||
|
fadeHorizontalLength = 10,
|
||||||
|
fadeVerticalLength = 10,
|
||||||
|
}: BackgroundProps) {
|
||||||
|
const { innerWidth, innerHeight, isLoaded, enterTransition } =
|
||||||
|
useChartStable();
|
||||||
|
const uniqueId = useId();
|
||||||
|
const patternId = `chart-background-${uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
|
const hStops = useMemo(
|
||||||
|
() => fadeMaskStops(fadeHorizontalLength),
|
||||||
|
[fadeHorizontalLength]
|
||||||
|
);
|
||||||
|
const vStops = useMemo(
|
||||||
|
() => fadeMaskStops(fadeVerticalLength),
|
||||||
|
[fadeVerticalLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pattern === "none" || !showFill || innerWidth <= 0 || innerHeight <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patternNode = renderPatternPreset(pattern, patternId, {
|
||||||
|
color,
|
||||||
|
scale,
|
||||||
|
strokeWidth,
|
||||||
|
radius,
|
||||||
|
complement,
|
||||||
|
fill,
|
||||||
|
dotFill,
|
||||||
|
tileBackground,
|
||||||
|
});
|
||||||
|
if (!patternNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fadeMask = fadeHorizontal || fadeVertical;
|
||||||
|
const hMaskId = `chart-background-fade-h-${uniqueId.replace(/:/g, "")}`;
|
||||||
|
const hGradientId = `${hMaskId}-gradient`;
|
||||||
|
const vMaskId = `chart-background-fade-v-${uniqueId.replace(/:/g, "")}`;
|
||||||
|
const vGradientId = `${vMaskId}-gradient`;
|
||||||
|
const combinedMaskId = `chart-background-fade-${uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
|
let maskRef: string | undefined;
|
||||||
|
if (fadeHorizontal && fadeVertical) {
|
||||||
|
maskRef = `url(#${combinedMaskId})`;
|
||||||
|
} else if (fadeHorizontal) {
|
||||||
|
maskRef = `url(#${hMaskId})`;
|
||||||
|
} else if (fadeVertical) {
|
||||||
|
maskRef = `url(#${vMaskId})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetOpacity = opacity;
|
||||||
|
const revealOpacity = isLoaded ? targetOpacity : 0;
|
||||||
|
const enterFade =
|
||||||
|
enterTransition && typeof enterTransition === "object"
|
||||||
|
? enterTransition
|
||||||
|
: { duration: BACKGROUND_ENTER_FADE_MS / 1000, ease: "easeOut" as const };
|
||||||
|
|
||||||
|
return (
|
||||||
|
// biome-ignore lint/a11y/noAriaHiddenOnFocusable: decorative plot-area pattern layer
|
||||||
|
<g aria-hidden="true" className="chart-background">
|
||||||
|
{fadeMask ? (
|
||||||
|
<defs>
|
||||||
|
{fadeHorizontal ? (
|
||||||
|
<>
|
||||||
|
<linearGradient
|
||||||
|
id={hGradientId}
|
||||||
|
x1="0%"
|
||||||
|
x2="100%"
|
||||||
|
y1="0%"
|
||||||
|
y2="0%"
|
||||||
|
>
|
||||||
|
{hStops.map((stop) => (
|
||||||
|
<stop
|
||||||
|
key={stop.offset}
|
||||||
|
offset={stop.offset}
|
||||||
|
style={{ stopColor: "white", stopOpacity: stop.opacity }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</linearGradient>
|
||||||
|
<mask id={hMaskId}>
|
||||||
|
<rect
|
||||||
|
fill={`url(#${hGradientId})`}
|
||||||
|
height={innerHeight}
|
||||||
|
width={innerWidth}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{fadeVertical ? (
|
||||||
|
<>
|
||||||
|
<linearGradient
|
||||||
|
id={vGradientId}
|
||||||
|
x1="0%"
|
||||||
|
x2="0%"
|
||||||
|
y1="0%"
|
||||||
|
y2="100%"
|
||||||
|
>
|
||||||
|
{vStops.map((stop) => (
|
||||||
|
<stop
|
||||||
|
key={stop.offset}
|
||||||
|
offset={stop.offset}
|
||||||
|
style={{ stopColor: "white", stopOpacity: stop.opacity }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</linearGradient>
|
||||||
|
<mask id={vMaskId}>
|
||||||
|
<rect
|
||||||
|
fill={`url(#${vGradientId})`}
|
||||||
|
height={innerHeight}
|
||||||
|
width={innerWidth}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{fadeHorizontal && fadeVertical ? (
|
||||||
|
<mask id={combinedMaskId}>
|
||||||
|
<g mask={`url(#${hMaskId})`}>
|
||||||
|
<rect
|
||||||
|
fill={`url(#${vGradientId})`}
|
||||||
|
height={innerHeight}
|
||||||
|
width={innerWidth}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
) : null}
|
||||||
|
</defs>
|
||||||
|
) : null}
|
||||||
|
<defs>{patternNode}</defs>
|
||||||
|
<motion.rect
|
||||||
|
animate={{ opacity: revealOpacity }}
|
||||||
|
fill={`url(#${patternId})`}
|
||||||
|
height={innerHeight}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
mask={maskRef}
|
||||||
|
transition={isLoaded ? enterFade : { duration: 0 }}
|
||||||
|
width={innerWidth}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Background.displayName = "Background";
|
||||||
|
|
||||||
|
export default Background;
|
||||||
38
seo_mode/seo_mode/SEO-farm/vendor/bklit-ui/packages/ui/src/charts/bar-chart-loading.tsx
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { BarChart } from "./bar-chart";
|
||||||
|
import type { Margin } from "./chart-context";
|
||||||
|
|
||||||
|
const EMPTY_DATA: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
|
export interface BarChartLoadingProps {
|
||||||
|
/** Chart margins. */
|
||||||
|
margin?: Partial<Margin>;
|
||||||
|
/** Aspect ratio as "width / height". Default: "2 / 1" */
|
||||||
|
aspectRatio?: string;
|
||||||
|
/** Additional class name for the container. */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turnkey loading skeleton for bar charts, a thin shortcut for
|
||||||
|
* `<BarChart status="loading" />`. Renders shimmer-swept placeholder bars while
|
||||||
|
* data is fetching; swap in a real `<BarChart>` once it resolves.
|
||||||
|
*/
|
||||||
|
export function BarChartLoading({
|
||||||
|
margin,
|
||||||
|
aspectRatio = "2 / 1",
|
||||||
|
className = "",
|
||||||
|
}: BarChartLoadingProps) {
|
||||||
|
return (
|
||||||
|
<BarChart
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
className={className}
|
||||||
|
data={EMPTY_DATA}
|
||||||
|
margin={margin}
|
||||||
|
status="loading"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BarChartLoading;
|
||||||