v{packageJson.version}
100 |101 | Developed and designed by Jasmin 106 |
107 |├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ ├── docs.yml │ ├── lint.yml │ └── release-please.yml ├── .gitignore ├── .release-please-manifest.json ├── LICENSE ├── README.md ├── biome.json ├── dev ├── index.html ├── package.json ├── src │ ├── ChartsLab.tsx │ ├── index.css │ └── index.tsx ├── tsconfig.json └── vite.config.ts ├── docs ├── .env.example ├── .prettierignore ├── .prettierrc.json ├── astro.config.ts ├── package.json ├── public │ ├── banner.jpg │ ├── favicon.ico │ ├── favicon.svg │ ├── fonts │ │ └── MonaspaceNeon-Regular-v1.101.woff2 │ ├── readme │ │ └── solid-charts.png │ └── robots.txt ├── src │ ├── assets │ │ ├── global.css │ │ ├── header_logo_dark.svg │ │ ├── header_logo_light.svg │ │ └── shiki.css │ ├── components │ │ ├── Background.tsx │ │ ├── Head.astro │ │ ├── docs │ │ │ ├── Link.astro │ │ │ ├── TableOfContents.tsx │ │ │ ├── api │ │ │ │ ├── ApiReference.astro │ │ │ │ ├── items │ │ │ │ │ ├── ApiItemComponent.astro │ │ │ │ │ └── ApiItemSimple.astro │ │ │ │ └── lists │ │ │ │ │ ├── ApiProps.astro │ │ │ │ │ └── ApiTag.astro │ │ │ ├── code │ │ │ │ ├── Code.astro │ │ │ │ ├── CopyToClipboard.tsx │ │ │ │ └── RawCode.astro │ │ │ ├── headings │ │ │ │ ├── H2.astro │ │ │ │ └── H3.astro │ │ │ └── sidebar │ │ │ │ ├── NavLink.astro │ │ │ │ └── Navigation.astro │ │ ├── index │ │ │ └── IndexChart.tsx │ │ ├── scripts │ │ │ ├── PlatformScript.astro │ │ │ ├── SidebarNavScript.astro │ │ │ └── ThemeScript.astro │ │ └── topbar │ │ │ ├── Drawer.tsx │ │ │ ├── ThemeSelect.tsx │ │ │ ├── Topbar.astro │ │ │ └── search │ │ │ ├── Search.tsx │ │ │ ├── SearchDialog.tsx │ │ │ └── SearchItem.tsx │ ├── env.d.ts │ ├── examples │ │ ├── ExampleWrapper.tsx │ │ ├── docs │ │ │ ├── axis │ │ │ │ ├── multiple.tsx │ │ │ │ └── single.tsx │ │ │ ├── chart │ │ │ │ ├── object-data.tsx │ │ │ │ └── simple.tsx │ │ │ ├── curves │ │ │ │ ├── cardinal.tsx │ │ │ │ └── step.tsx │ │ │ ├── series │ │ │ │ ├── active-point.tsx │ │ │ │ ├── area.tsx │ │ │ │ ├── bar.tsx │ │ │ │ ├── line.tsx │ │ │ │ └── point.tsx │ │ │ ├── stacks │ │ │ │ ├── area.tsx │ │ │ │ └── bar.tsx │ │ │ └── usage │ │ │ │ └── first.tsx │ │ └── examples │ │ │ └── temp.tsx │ ├── layouts │ │ └── Docs.astro │ ├── lib │ │ └── typedoc │ │ │ ├── apiPages.ts │ │ │ ├── headings.ts │ │ │ ├── resolve │ │ │ ├── lib.ts │ │ │ ├── resolve.ts │ │ │ ├── resolveComponent.ts │ │ │ └── resolveSimple.ts │ │ │ └── types │ │ │ ├── apiReferences.ts │ │ │ ├── specifications.ts │ │ │ └── typedoc.ts │ └── pages │ │ ├── 404.astro │ │ ├── docs │ │ ├── axis.mdx │ │ ├── components │ │ │ ├── axis.mdx │ │ │ ├── chart.mdx │ │ │ └── series.mdx │ │ ├── curves.mdx │ │ ├── index.mdx │ │ ├── stacks.mdx │ │ └── usage.mdx │ │ ├── examples │ │ └── index.mdx │ │ └── index.astro ├── tsconfig.json ├── typesense.config.json └── vercel.json ├── package.json ├── packages └── solid-charts │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── axis │ │ ├── Axis.tsx │ │ ├── Cursor.tsx │ │ ├── Grid.tsx │ │ ├── Label.tsx │ │ ├── Line.tsx │ │ ├── Mark.tsx │ │ ├── Tooltip.tsx │ │ ├── ValueLine.tsx │ │ └── context.ts │ ├── components │ │ ├── Chart.tsx │ │ └── context.ts │ ├── curves.ts │ ├── index.ts │ ├── lib │ │ ├── createBands.ts │ │ ├── createBaseLine.ts │ │ ├── createClosestTick.ts │ │ ├── createLabelTicks.ts │ │ ├── createPoints.ts │ │ ├── createScale.ts │ │ ├── createSeries.ts │ │ ├── createTicks.ts │ │ ├── dom │ │ │ ├── charSize.ts │ │ │ ├── createSize.ts │ │ │ └── createSvgSize.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── series │ │ ├── Area.tsx │ │ ├── Bar.tsx │ │ ├── Line.tsx │ │ └── Point.tsx │ └── shapes │ │ └── Curve.tsx │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── release-please-config.json └── turbo.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: GiyoMoon 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug report' 2 | description: Create a report to help improve solid-charts 3 | title: "[Bug]: " 4 | labels: ["bug (unverified)"] 5 | assignees: 6 | - GiyoMoon 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thank you for reporting a bug to solid-charts :pray::purple_heart: 12 | 13 | Before submitting a new bug/issue, please make sure to search for existing issues to avoid submitting duplicates: 14 | 15 | - [open issues](https://github.com/corvudev/solid-charts/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated) 16 | - [closed issues](https://github.com/corvudev/solid-charts/issues?q=is%3Aissue+is%3Aclosed+sort%3Aupdated+) 17 | - [solid-charts discussions](https://github.com/corvudev/solid-charts/discussions) 18 | - type: textarea 19 | id: description 20 | attributes: 21 | label: Bug description 22 | description: Please provide a clear description of what the bug is. 23 | validations: 24 | required: true 25 | - type: input 26 | id: link 27 | attributes: 28 | label: Reproduction Link 29 | description: | 30 | Provide a link to a live example, or a repository that can reproduce the bug. We can analyze and fix the bug much faster if you provide a clear and minimal example. Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 31 | 32 | You can use [Stackblitz](https://stackblitz.com/) to create a sharable reproduction. A public GitHub repository also works. 33 | placeholder: | 34 | https://stackblitz.com/edit/... or a public GitHub repository 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: steps 39 | attributes: 40 | label: Reproduction Steps 41 | description: Describe how we can reproduce the bug with the provided link. 42 | placeholder: | 43 | 1. Click on ... 44 | 2. Open the console 45 | 3. See the error message 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: expected 50 | attributes: 51 | label: Expected behavior 52 | description: Provide a clear description of what you expected to happen. 53 | placeholder: | 54 | I expected ... but ... is happening instead. 55 | validations: 56 | required: true 57 | - type: textarea 58 | id: additional 59 | attributes: 60 | label: Additional context 61 | description: Add any other context about the bug here. 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@v4 14 | 15 | - name: Install Vercel CLI 16 | run: npm install -g vercel@37.8.0 17 | 18 | - name: Deploy to vercel 19 | uses: BetaHuhn/deploy-to-vercel-action@v1 20 | with: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} 23 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 24 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 25 | 26 | - name: Typesense scraper 27 | uses: celsiusnarhwal/typesense-scraper@v2 28 | with: 29 | api-key: ${{ secrets.TYPESENSE_API_KEY }} 30 | host: search.solidcharts.dev 31 | port: 443 32 | protocol: https 33 | config: ./docs/typesense.config.json 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out 17 | uses: actions/checkout@v4 18 | 19 | - name: Install nodejs 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: 10.7.1 28 | run_install: false 29 | 30 | - name: Get pnpm store dir 31 | id: pnpm-cache 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 35 | 36 | - name: pnpm cache 37 | uses: actions/cache@v4 38 | with: 39 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install dependencies 45 | run: | 46 | pnpm install 47 | 48 | - name: Lint 49 | run: | 50 | pnpm lint 51 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: release-please 13 | id: release-please 14 | uses: googleapis/release-please-action@v4 15 | 16 | - name: Check out 17 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 18 | uses: actions/checkout@v4 19 | 20 | - name: Install nodejs 21 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Install pnpm 27 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 28 | uses: pnpm/action-setup@v4 29 | with: 30 | version: 10.7.1 31 | run_install: false 32 | 33 | - name: Get pnpm store dir 34 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 35 | id: pnpm-cache 36 | shell: bash 37 | run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 38 | 39 | - name: pnpm cache 40 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 41 | uses: actions/cache@v4 42 | with: 43 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 44 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 45 | restore-keys: | 46 | ${{ runner.os }}-pnpm-store- 47 | 48 | - name: Install dependencies 49 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 50 | run: pnpm install --frozen-lockfile 51 | 52 | - name: Set pnpm auth token 53 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 54 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" 55 | env: 56 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 57 | 58 | - name: Publish to npm 59 | if: ${{ steps.release-please.outputs.releases_created == 'true' }} 60 | run: pnpm ci:publish 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .astro 2 | .env 3 | .turbo 4 | dist 5 | node_modules 6 | packages/*/README.md 7 | tsup.config.*.mjs 8 | api.json 9 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {"packages/solid-charts":"0.0.1"} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jasmin Noetzli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
Property | 18 |Default | 19 |Type/Description | 20 |
---|---|---|
25 | 34 | {prop.name} 35 | 36 | |
37 |
38 | {prop.defaultHtml === null ? (
39 | - 40 | ) : ( 41 | 45 | )} 46 | |
47 |
48 | 49 | {prop.type} 50 | 51 | 52 | |
53 |
60 | No additional props. 61 | |
62 |
20 | Data attributes present on <{componentName} />
{' '}
21 | components.
22 |
28 | CSS properties present on <{componentName} />
{' '}
29 | components.
30 |
Property | 37 |Description | 38 |
---|---|
43 | 44 | {prop.name} 45 | 46 | |
47 | 48 | 49 | | 50 |
{props.data.xAxis}
42 |Value
46 |{props.data.value}
47 |{props.data.xAxis}
48 |Value 1
52 |{props.data.value1}
53 |Value 2
57 |{props.data.value2}
58 |{props.data.xAxis}
42 |Value 1
46 |{props.data.value1}
47 |Value 2
51 |{props.data.value2}
52 |{props.data.xAxis}
41 |Value
45 |{props.data.value}
46 |v{packageJson.version}
100 |101 | Developed and designed by Jasmin 106 |
107 |
36 |
37 | This will render this simple line chart:
38 |
39 |
50 |
51 | This will render a chart with two series and an x-axis label:
52 |
53 |
70 |
71 | ...
72 |
73 |
74 | `} lang="tsx" />
75 |
76 | **Aspect ratio**
77 |
78 | This chart will render with the size of the parent container, adopting its aspect ratio of 4:3.
79 |
86 |
87 | ...
88 |
89 |
90 | `} lang="tsx" />
91 |
92 | ## Inset
93 |
94 | By default, the chart will render with an inset padding of 8px. This is to accommodate for line series with curve interpolation that might overflow the chart area. You can modify this inset or remove it with the `inset` property:
95 |
96 |
105 | `} lang="tsx" />
106 |
107 | This sets the top inset to 16px and removes the other insets. To remove all insets, you can provide a single value:
108 |
109 |
111 | `} lang="tsx" />
112 |
113 | ## API Reference
114 |
115 |
116 |
--------------------------------------------------------------------------------
/docs/src/pages/docs/components/series.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: Series
4 | description: Composable chart library for SolidJS
5 | image: https://solidcharts.dev/banner.jpg
6 | apiPageName: Series
7 | ---
8 | import Link from '@components/docs/Link.astro'
9 | import H2 from '@components/docs/headings/H2.astro'
10 | import H3 from '@components/docs/headings/H3.astro'
11 | export const components = { h2: H2, h3: H3 }
12 | import Code from '@components/docs/code/Code.astro'
13 | import ExampleWrapper from '@examples/ExampleWrapper'
14 | import RawCode from '@components/docs/code/RawCode.astro'
15 | import LineExample from '@examples/docs/series/line'
16 | import LineExampleTsx from '@examples/docs/series/line?raw'
17 | import AreaExample from '@examples/docs/series/area'
18 | import AreaExampleTsx from '@examples/docs/series/area?raw'
19 | import PointExample from '@examples/docs/series/point'
20 | import PointExampleTsx from '@examples/docs/series/point?raw'
21 | import BarExample from '@examples/docs/series/bar'
22 | import BarExampleTsx from '@examples/docs/series/bar?raw'
23 | import ActivePointExample from '@examples/docs/series/active-point'
24 | import ActivePointExampleTsx from '@examples/docs/series/active-point?raw'
25 | import ApiReference from '@components/docs/api/ApiReference.astro'
26 | import ApiPages from '@lib/typedoc/apiPages'
27 |
28 | # Series
29 |
30 | ## <Line>
31 |
32 |
41 |
42 |
43 |
44 |
45 | ## <Area>
46 |
47 |
56 |
57 |
58 |
59 |
60 | ## <Point>
61 |
62 | The point series is used to render `` elements representing the values in the chart. Is often used in combination with the `Line` or `Area` series to highlight the data points on the chart.
63 |
64 |
73 |
74 |
75 |
76 |
77 | ### activeProps
78 |
79 | The point series accepts the `activeProps` object to customize the appearance of the currently active `` element. This allows you to do things like animating the radius of an active point:
80 |
81 |
82 |
83 |
84 | Hover over the chart:
85 |
86 |
87 |
88 |
89 |
90 |
91 | ## <Bar>
92 |
93 |
102 |
103 |
104 |
105 |
106 | ### barConfig
107 |
108 | For the bar series there is a global `barConfig` object on the `` component that lets you modify the way multiple bar series are rendere together. It's used to define values like the space between bars or their width. Have a look at the `BarConfig` type in the Chart API reference.
109 |
110 | ## API Reference
111 |
112 |
113 |
--------------------------------------------------------------------------------
/docs/src/pages/docs/curves.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: Curves
4 | description: Composable chart library for SolidJS
5 | image: https://solidcharts.dev/banner.jpg
6 | ---
7 | import Link from '@components/docs/Link.astro'
8 | import H2 from '@components/docs/headings/H2.astro'
9 | import H3 from '@components/docs/headings/H3.astro'
10 | export const components = { h2: H2, h3: H3 }
11 | import Code from '@components/docs/code/Code.astro'
12 | import StepCurveExample from '@examples/docs/curves/step'
13 | import CardinalCurveExample from '@examples/docs/curves/cardinal'
14 |
15 | # Curves
16 |
17 | solid-charts uses D3's curves to generate the line path for the Area and Line series. You can use any of the available curve functions or provide your custom function to customize the appearance of your line chart. The default curve is `curveLinear`, which creates a straight path between points. The curve functions are re-exported from D3 under `solid-charts/curves`.
18 |
19 | ## Available curves
20 |
21 | All the curves are documented in D3's curves documentation.
22 |
23 |
46 |
47 | ## Examples
48 |
49 | ### Step
50 |
51 |
55 | `} lang="tsx" />
56 |
57 |
58 |
59 |
60 |
61 | ### Cardinal
62 |
63 |
67 | `} lang="tsx" />
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/docs/src/pages/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: Introduction to solid-charts
4 | description: An overview of solid-charts and its features.
5 | image: https://solidcharts.dev/banner.jpg
6 | ---
7 | import Link from '@components/docs/Link.astro'
8 | import H2 from '@components/docs/headings/H2.astro'
9 | import H3 from '@components/docs/headings/H3.astro'
10 | export const components = { h2: H2, h3: H3 }
11 |
12 | # Introduction
13 |
14 | solid-charts is a component-driven chart library for SolidJS. It's based on SVG elements and uses D3 for data visualization.
15 |
16 | ## Features
17 |
18 | ### Composable
19 |
20 | The library offers components like ``, ``, and ``, enabling you to compose charts the way you want. Unused components are tree-shaken away to ensure the smallest bundle possible.
21 |
22 | ### Unstyled
23 |
24 | solid-charts renders default, unstyled SVG elements, giving you complete control over the appearance of your charts. This makes it easy to integrate the charts into your design system. Whether you're using Tailwind CSS, a CSS-in-JS solution, or plain CSS, styling is entirely up to you.
25 |
26 | ## Development status
27 |
28 | solid-charts is currently in a very early stage of development. The goal is to introduce the library to developers and ask for initial feedback. There are a lot of planned features yet to be implemented and the API is bound to iterate and change.
29 |
30 | ### Roadmap
31 |
32 | A very rough overview of planned features that aren't implemented yet. The list isn't final and changes depending on user feedback and targeted scope of the library.
33 |
34 | **- More chart types**
35 |
36 | Currently the library supports the most common chart types (Line, Area, Bar, Point). The goal is to support a few more like Pie, Radar and RadialBar.
37 |
38 | **- Better customizablility of the y axis**
39 |
40 | The goal is to treat the y axis the same way as the x axis behaves. It's currently not possible to have a vertical chart and have the x axis correspond to the value of the series. This should allow for two dimensional series too where both axis align to a value.
41 |
42 | **- Some kind of support for animations**
43 |
44 | Animating the charts hasn't been explored yet. Native svg animations should work already, but maybe there is need for more support.
45 |
46 | **- SSR**
47 |
48 | Due to the composable nature and working with contexts to register parts of the chart, it's not possible to render the chart on the server. Goal is to explore the options here and see what could be possible.
49 |
50 | ## Community
51 |
52 | If you have feedback, questions, feature requests or just want to write a kind message, feel free to join the SolidJS Discord server. We are always happy to help and love to hear from you. Alternatively, you can leave your feedback in the discussions tab on the GitHub repository.
53 |
--------------------------------------------------------------------------------
/docs/src/pages/docs/stacks.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: Stacks
4 | description: Composable chart library for SolidJS
5 | image: https://solidcharts.dev/banner.jpg
6 | ---
7 | import Link from '@components/docs/Link.astro'
8 | import H2 from '@components/docs/headings/H2.astro'
9 | import H3 from '@components/docs/headings/H3.astro'
10 | export const components = { h2: H2, h3: H3 }
11 | import Code from '@components/docs/code/Code.astro'
12 | import ExampleWrapper from '@examples/ExampleWrapper'
13 | import RawCode from '@components/docs/code/RawCode.astro'
14 | import AreaExample from '@examples/docs/stacks/area'
15 | import AreaExampleTsx from '@examples/docs/stacks/area?raw'
16 | import BarExample from '@examples/docs/stacks/bar'
17 | import BarExampleTsx from '@examples/docs/stacks/bar?raw'
18 |
19 | # Stacks
20 |
21 | You might want to stack your series on top of each other to visualize a total value. Series components accept the `stackId` property for this. Provide the same stack id to all the series you want to have stacked together.
22 |
23 | ## Area chart
24 |
25 | This chart utilizes the Area, Line and Point series to render two stacked data series.
26 |
27 |
36 |
37 |
38 |
39 |
40 | Hover over the chart to see their respective values.
41 |
42 | ## Bar chart
43 |
44 | Bar series behave the same and can be stacked too:
45 |
46 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/src/pages/docs/usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: Usage
4 | description: Learn how to use solid-charts
5 | image: https://solidcharts.dev/banner.jpg
6 | ---
7 | import Link from '@components/docs/Link.astro'
8 | import H2 from '@components/docs/headings/H2.astro'
9 | import H3 from '@components/docs/headings/H3.astro'
10 | export const components = { h2: H2, h3: H3 }
11 | import Code from '@components/docs/code/Code.astro'
12 | import ExampleWrapper from '@examples/ExampleWrapper'
13 | import RawCode from '@components/docs/code/RawCode.astro'
14 | import FirstExample from '@examples/docs/usage/first'
15 | import FirstExampleTsx from '@examples/docs/usage/first?raw'
16 |
17 | # Usage
18 |
19 | ## Installation
20 |
21 | Install the library with the package manager of your choice.
22 |
23 |
26 |
27 | ## Creating your first chart
28 |
29 | Let's create a chart! We're using a simple dataset to draw a line chart that includes both an x-axis and y-axis. You'll also find a tooltip and a cursor line that appears when you hover over a specific day, making it easier to visualize and interact with your data.
30 |
31 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/docs/src/pages/examples/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | layout: '@layouts/Docs.astro'
3 | title: solid-charts examples
4 | description: A collection of examples showcasing the capabilities of solid-charts.
5 | image: https://solidcharts.dev/banner.jpg
6 | ---
7 | import Link from '@components/docs/Link.astro'
8 | import H2 from '@components/docs/headings/H2.astro'
9 | import H3 from '@components/docs/headings/H3.astro'
10 | export const components = { h2: H2, h3: H3 }
11 | import TempExample from '@examples/examples/temp'
12 |
13 | # Examples
14 |
15 | These are temporary examples to show what's possible with solid-charts. More and better examples with code will be added soon!
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import '@fontsource-variable/rubik/wght.css'
3 | import Background from '@components/Background'
4 | import '@assets/global.css'
5 | import Head from '@components/Head.astro'
6 | import IndexChart from '@components/index/IndexChart'
7 | import PlatformScript from '@components/scripts/PlatformScript.astro'
8 | import ThemeScript from '@components/scripts/ThemeScript.astro'
9 | import Topbar from '@components/topbar/Topbar.astro'
10 | import rubikWoff2 from '@fontsource-variable/rubik/files/rubik-latin-wght-normal.woff2?url'
11 | ---
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
33 |
40 |
41 |
42 |
48 |
54 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | solid-charts
69 | - composable charts for SolidJS
70 |
71 |
72 | Component based svg charts for SolidJS. Composable, unstyled, and
73 | tweakable.
74 |
75 | Get started
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "jsxImportSource": "solid-js",
6 | "baseUrl": ".",
7 | "paths": {
8 | "@assets/*": ["./src/assets/*"],
9 | "@components/*": ["./src/components/*"],
10 | "@examples/*": ["./src/examples/*"],
11 | "@layouts/*": ["./src/layouts/*"],
12 | "@lib/*": ["./src/lib/*"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docs/typesense.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "index_name": "solidcharts",
3 | "start_urls": [
4 | "https://solidcharts.dev/docs/",
5 | "https://solidcharts.dev/examples/"
6 | ],
7 | "sitemap_urls": ["https://solidcharts.dev/sitemap-index.xml"],
8 | "selectors": {
9 | "lvl0": "article h1",
10 | "lvl1": "article h2",
11 | "lvl2": "article h3",
12 | "lvl3": "article h4",
13 | "text": "article p, article li"
14 | },
15 | "selectors_exclude": ["[data-typesense-ignore]"],
16 | "nb_hits": 0
17 | }
18 |
--------------------------------------------------------------------------------
/docs/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/plausible/js/script.js",
5 | "destination": "https://plausible.io/js/script.js"
6 | },
7 | {
8 | "source": "/plausible/api/event/",
9 | "destination": "https://plausible.io/api/event"
10 | },
11 | {
12 | "source": "/lythia/a.js",
13 | "destination": "https://app.lythia.dev/collect/analytics/script.js"
14 | },
15 | {
16 | "source": "/lythia/a/event/",
17 | "destination": "https://app.lythia.dev/collect/analytics/report"
18 | },
19 | {
20 | "source": "/lythia/m.js",
21 | "destination": "https://app.lythia.dev/collect/metrics/script.js"
22 | },
23 | {
24 | "source": "/lythia/m/event/",
25 | "destination": "https://app.lythia.dev/collect/metrics/report"
26 | }
27 | ],
28 | "trailingSlash": true,
29 | "headers": [
30 | {
31 | "source": "/fonts/MonaspaceNeon-Regular-v1.101.woff2",
32 | "headers": [
33 | {
34 | "key": "Cache-Control",
35 | "value": "public, max-age=31536000, immutable"
36 | }
37 | ]
38 | }
39 | ],
40 | "git": {
41 | "deploymentEnabled": {
42 | "main": false
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solid-charts/monorepo",
3 | "private": true,
4 | "license": "MIT",
5 | "author": {
6 | "name": "Jasmin Noetzli",
7 | "email": "code@jasi.dev",
8 | "url": "https://github.com/GiyoMoon"
9 | },
10 | "scripts": {
11 | "build": "turbo run build",
12 | "ci:publish": "pnpm build && pnpm publish -r --access public",
13 | "clean": "turbo run clean && rm -rf .turbo node_modules",
14 | "dev:charts": "turbo watch dev --filter=@solid-charts/dev",
15 | "dev:docs": "turbo watch dev --filter=@solid-charts/docs",
16 | "lint": "turbo run lint",
17 | "lint:fix": "turbo run lint:fix",
18 | "preview:charts": "turbo run preview --filter=@solid-charts/dev",
19 | "preview:docs": "turbo run preview --filter=@solid-charts/docs",
20 | "rp": "release-please"
21 | },
22 | "devDependencies": {
23 | "@biomejs/biome": "^1.9.4",
24 | "release-please": "^17.0.0",
25 | "turbo": "^2.5.3"
26 | },
27 | "packageManager": "pnpm@10.7.1"
28 | }
29 |
--------------------------------------------------------------------------------
/packages/solid-charts/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.0.1 (2025-05-07)
4 |
5 | - Initial release
6 |
--------------------------------------------------------------------------------
/packages/solid-charts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "solid-charts",
3 | "version": "0.0.1",
4 | "private": false,
5 | "description": "Composable charts for SolidJS",
6 | "keywords": [
7 | "solid",
8 | "solidjs",
9 | "d3",
10 | "charts"
11 | ],
12 | "bugs": {
13 | "url": "https://github.com/corvudev/solid-charts/issues",
14 | "email": "code@jasi.dev"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/corvudev/solid-charts.git"
19 | },
20 | "license": "MIT",
21 | "author": {
22 | "name": "Jasmin Noetzli",
23 | "email": "code@jasi.dev",
24 | "url": "https://github.com/GiyoMoon"
25 | },
26 | "sideEffects": false,
27 | "type": "module",
28 | "exports": {
29 | ".": {
30 | "types": "./dist/index.d.ts",
31 | "solid": "./dist/index.jsx",
32 | "default": "./dist/index.js"
33 | },
34 | "./curves": {
35 | "types": "./dist/curves.d.ts",
36 | "solid": "./dist/curves.jsx",
37 | "default": "./dist/curves.js"
38 | }
39 | },
40 | "main": "./dist/index.js",
41 | "types": "./dist/index.d.ts",
42 | "typesVersions": {
43 | "*": {
44 | "*": [
45 | "./dist/index.d.ts"
46 | ]
47 | }
48 | },
49 | "files": [
50 | "dist"
51 | ],
52 | "scripts": {
53 | "build": "cp ../../README.md . && tsc --noEmit && tsup",
54 | "clean": "rm -rf .turbo dist node_modules",
55 | "dev": "tsup --watch",
56 | "lint": "biome check",
57 | "lint:fix": "biome check --fix",
58 | "typedoc": "typedoc --json api.json --entryPoints ./src/index.ts"
59 | },
60 | "dependencies": {
61 | "@corvu/utils": "^0.4.2",
62 | "@solid-primitives/memo": "^1.4.2",
63 | "d3-scale": "^4.0.2",
64 | "d3-shape": "^3.2.0"
65 | },
66 | "devDependencies": {
67 | "@types/d3-scale": "^4.0.9",
68 | "@types/d3-shape": "^3.1.7",
69 | "esbuild-plugin-solid": "^0.6.0",
70 | "solid-js": "^1.9.6",
71 | "tsup": "^8.4.0",
72 | "typedoc": "^0.28.4",
73 | "typescript": "^5.8.3"
74 | },
75 | "peerDependencies": {
76 | "solid-js": "^1.8"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Axis.tsx:
--------------------------------------------------------------------------------
1 | import { createWritableMemo } from '@solid-primitives/memo'
2 | import { AxisContext } from '@src/axis/context'
3 | import { useChartContext } from '@src/components/context'
4 | import createScale, { type ScaleType } from '@src/lib/createScale'
5 | import createTicks from '@src/lib/createTicks'
6 | import { accessData } from '@src/lib/utils'
7 | import {
8 | type JSX,
9 | createEffect,
10 | createMemo,
11 | mergeProps,
12 | onCleanup,
13 | } from 'solid-js'
14 |
15 | export type AxisProps = {
16 | /**
17 | * The key in the data object to use for this axis. Don't provide a key if the data is a simple array of numbers.
18 | */
19 | dataKey?: string
20 | /**
21 | * The id of the axis.
22 | * @defaultValue '0'
23 | */
24 | axisId?: string
25 | /**
26 | * The scale type of the axis.
27 | */
28 | type?: ScaleType
29 | /**
30 | * The count of axis ticks.
31 | */
32 | tickCount?: number
33 | /**
34 | * Force specific tick values.
35 | */
36 | tickValues?: any[]
37 | /**
38 | * The range of the axis. Only used when `type` is set to `'linear'`.
39 | * @defaultValue 'auto'
40 | */
41 | axisRange?: 'auto' | [number | 'min', number | 'max']
42 | /** @hidden */
43 | children?: JSX.Element
44 | } & (XAxisProps | YAxisProps)
45 |
46 | export type XAxisProps = {
47 | /**
48 | * The axis type.
49 | */
50 | axis: 'x'
51 | /**
52 | * The position of the axis labels.
53 | */
54 | position: 'top' | 'bottom'
55 | }
56 |
57 | export type YAxisProps = {
58 | /**
59 | * The axis type.
60 | */
61 | axis: 'y'
62 | /**
63 | * The position of the axis labels.
64 | */
65 | position: 'left' | 'right'
66 | }
67 |
68 | export type { ScaleType }
69 |
70 | /** Context provider for axis components. */
71 | const Axis = (props: AxisProps) => {
72 | const defaultedProps = mergeProps(
73 | {
74 | type: props.axis === 'x' ? ('categorial' as const) : ('linear' as const),
75 | axisId: '0',
76 | tickCount: 5,
77 | axisRange: 'auto' as const,
78 | },
79 | props,
80 | )
81 | const chartContext = useChartContext()
82 |
83 | const data = createMemo(() => {
84 | return accessData(chartContext.data(), defaultedProps.dataKey)
85 | })
86 |
87 | createEffect(() => {
88 | const axisRange = defaultedProps.axisRange
89 | if (axisRange === 'auto') return
90 | chartContext.registerAxis(defaultedProps.axisId, {
91 | type: 'user',
92 | range: axisRange,
93 | })
94 | onCleanup(() =>
95 | chartContext.unregisterAxis(defaultedProps.axisId, { type: 'user' }),
96 | )
97 | })
98 |
99 | const scale = createScale({
100 | axis: () => defaultedProps.axis,
101 | type: () => defaultedProps.type,
102 | axisId: () => defaultedProps.axisId,
103 | data,
104 | chartContext,
105 | })
106 |
107 | const ticks = createTicks({
108 | scale,
109 | tickCount: () => defaultedProps.tickCount,
110 | tickValues: () => defaultedProps.tickValues,
111 | })
112 |
113 | const [labelTicks, setLabelTicks] = createWritableMemo(() => ticks())
114 |
115 | return (
116 | defaultedProps.axis,
119 | position: () => defaultedProps.position,
120 | scale,
121 | ticks,
122 | labelTicks,
123 | setLabelTicks,
124 | }}
125 | >
126 | {defaultedProps.children}
127 |
128 | )
129 | }
130 |
131 | export default Axis
132 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Cursor.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import createClosestTick from '@src/lib/createClosestTick'
4 | import { type ComponentProps, mergeProps } from 'solid-js'
5 | import { isDev } from 'solid-js/web'
6 |
7 | export type CursorProps = Omit<
8 | ComponentProps<'line'>,
9 | 'x1' | 'y1' | 'x2' | 'y2'
10 | >
11 |
12 | /** Guide line rendered when hovering over the chart to hightlight the closest tick.
13 | *
14 | * @data `data-sc-axis-cursor` - Present on every cursor line element.
15 | */
16 | const Cursor = (props: CursorProps) => {
17 | const chartContext = useChartContext()
18 | const axisContext = useAxisContext()
19 |
20 | if (isDev && axisContext.scale().type === 'linear') {
21 | throw new Error(
22 | '[solid-charts] Cursor can not be used with an axis of type linear',
23 | )
24 | }
25 |
26 | const defaultedProps = mergeProps(
27 | {
28 | stroke: 'currentColor',
29 | 'stroke-width': 1,
30 | },
31 | props,
32 | )
33 |
34 | const closestTick = createClosestTick({
35 | axis: axisContext.axis,
36 | chartContext,
37 | })
38 |
39 | const x = () => {
40 | const tick = closestTick()
41 | if (!tick) return undefined
42 |
43 | switch (axisContext.axis()) {
44 | case 'x':
45 | return tick.position
46 | case 'y':
47 | return chartContext.getInset('left')
48 | }
49 | }
50 |
51 | const y = () => {
52 | const tick = closestTick()
53 | if (!tick) return undefined
54 |
55 | switch (axisContext.axis()) {
56 | case 'x':
57 | return chartContext.getInset('top')
58 | case 'y':
59 | return tick.position
60 | }
61 | }
62 |
63 | const x2 = () => {
64 | const tick = closestTick()
65 | if (!tick) return undefined
66 |
67 | switch (axisContext.axis()) {
68 | case 'x':
69 | return tick.position
70 | case 'y':
71 | return chartContext.width() - chartContext.getInset('right')
72 | }
73 | }
74 |
75 | const y2 = () => {
76 | const tick = closestTick()
77 | if (!tick) return undefined
78 |
79 | switch (axisContext.axis()) {
80 | case 'x':
81 | return chartContext.height() - chartContext.getInset('bottom')
82 | case 'y':
83 | return tick.position
84 | }
85 | }
86 |
87 | return (
88 |
97 | )
98 | }
99 |
100 | export default Cursor
101 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Grid.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import { type ComponentProps, For, mergeProps } from 'solid-js'
4 |
5 | export type GridProps = Omit, 'x1' | 'y1' | 'x2' | 'y2'>
6 |
7 | /** Lines displaying the axis ticks on the chart.
8 | *
9 | * @data `data-sc-axis-grid-group` - Present on every grid group element.
10 | * @data `data-sc-axis-grid` - Present on every grid line element.
11 | */
12 | const Grid = (props: GridProps) => {
13 | const chartContext = useChartContext()
14 | const axisContext = useAxisContext()
15 |
16 | const defaultedProps = mergeProps(
17 | {
18 | stroke: 'currentColor',
19 | 'stroke-width': 1,
20 | },
21 | props,
22 | )
23 |
24 | const x = (tick: any) => {
25 | switch (axisContext.axis()) {
26 | case 'x':
27 | return axisContext.scale().scale(tick)
28 | case 'y':
29 | return chartContext.getInset('left')
30 | }
31 | }
32 |
33 | const y = (tick: any) => {
34 | switch (axisContext.axis()) {
35 | case 'x':
36 | return chartContext.getInset('top')
37 | case 'y':
38 | return axisContext.scale().scale(tick)
39 | }
40 | }
41 |
42 | const x2 = (tick: any) => {
43 | switch (axisContext.axis()) {
44 | case 'x':
45 | return axisContext.scale().scale(tick)
46 | case 'y':
47 | return chartContext.width() - chartContext.getInset('right')
48 | }
49 | }
50 |
51 | const y2 = (tick: any) => {
52 | switch (axisContext.axis()) {
53 | case 'x':
54 | return chartContext.height() - chartContext.getInset('bottom')
55 | case 'y':
56 | return axisContext.scale().scale(tick)
57 | }
58 | }
59 |
60 | return (
61 |
62 |
63 | {(tick) => (
64 |
72 | )}
73 |
74 |
75 | )
76 | }
77 |
78 | export default Grid
79 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Label.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import createLabelTicks from '@src/lib/createLabelTicks'
4 | import { getAverageCharSize } from '@src/lib/dom/charSize'
5 | import createSvgSize from '@src/lib/dom/createSvgSize'
6 | import type { OverrideProps } from '@src/lib/types'
7 | import {
8 | type ComponentProps,
9 | For,
10 | createEffect,
11 | createSignal,
12 | createUniqueId,
13 | mergeProps,
14 | splitProps,
15 | } from 'solid-js'
16 |
17 | export type LabelProps = OverrideProps<
18 | Omit, 'x' | 'y'>,
19 | {
20 | /**
21 | * Optional function to format the label text.
22 | * @defaultValue (value) => String(value)
23 | */
24 | format?: (value: any) => string
25 | /**
26 | * The interval at which to show labels.
27 | * @defaultValue 'preserveEnd'
28 | */
29 | interval?: 'preserveStart' | 'preserveEnd' | 'preserveStartEnd' | number
30 | /**
31 | * The minimum gap between labels.
32 | * @defaultValue 16px
33 | */
34 | labelGap?: number
35 | }
36 | >
37 |
38 | /** Axis label component.
39 | *
40 | * @data `data-sc-axis-label-group` - Present on every label group element.
41 | * @data `data-sc-axis-label` - Present on every label text element.
42 | */
43 | const Label = (props: LabelProps) => {
44 | const chartContext = useChartContext()
45 | const axisContext = useAxisContext()
46 |
47 | const defaultedProps = mergeProps(
48 | {
49 | format: (value: any) => String(value),
50 | interval: 'preserveEnd' as const,
51 | labelGap: 16,
52 | fill: 'currentColor',
53 | 'text-anchor':
54 | axisContext.position() === 'left'
55 | ? ('end' as const)
56 | : axisContext.position() === 'right'
57 | ? ('start' as const)
58 | : ('middle' as const),
59 | 'dominant-baseline':
60 | axisContext.axis() === 'y' ? ('central' as const) : undefined,
61 | dx:
62 | axisContext.position() === 'left'
63 | ? '-0.5em'
64 | : axisContext.position() === 'right'
65 | ? '0.5em'
66 | : undefined,
67 | dy:
68 | axisContext.position() === 'bottom'
69 | ? '0.3em'
70 | : axisContext.position() === 'bottom'
71 | ? '-0.3em'
72 | : undefined,
73 | },
74 | props,
75 | )
76 | const [localProps, otherProps] = splitProps(defaultedProps, [
77 | 'format',
78 | 'interval',
79 | 'labelGap',
80 | ])
81 |
82 | const [labelGroupRef, setLabelGroupRef] = createSignal(
83 | null,
84 | )
85 |
86 | createSvgSize({
87 | element: labelGroupRef,
88 | dimension: () => (axisContext.axis() === 'x' ? 'height' : 'width'),
89 | onSizeChange: (size: number) => {
90 | chartContext.registerInset(
91 | axisContext.position(),
92 | 'axis.label',
93 | chartContext.toSvgPosition(
94 | size,
95 | axisContext.axis() === 'x' ? 'height' : 'width',
96 | ),
97 | )
98 | },
99 | onCleanup: () =>
100 | chartContext.unregisterInset(axisContext.position(), 'axis.label'),
101 | })
102 |
103 | const labelAxisId = createUniqueId()
104 |
105 | // Can't do DOM stuff in a memo in Solid 1.0
106 | const [averageCharSize, setAverageCharSize] = createSignal({
107 | x: 0,
108 | y: 0,
109 | })
110 | createEffect(() => {
111 | const _labelGroupRef = labelGroupRef()
112 | if (!_labelGroupRef) return
113 | setAverageCharSize(
114 | getAverageCharSize(_labelGroupRef, otherProps, labelAxisId),
115 | )
116 | })
117 |
118 | createLabelTicks({
119 | ticks: axisContext.ticks,
120 | interval: () => localProps.interval,
121 | labelGap: () => localProps.labelGap,
122 | format: () => localProps.format,
123 | averageCharSize,
124 | chartContext,
125 | axisContext,
126 | })
127 |
128 | const x = (tick: any) => {
129 | switch (axisContext.position()) {
130 | case 'top':
131 | case 'bottom': {
132 | const tickPosition = axisContext.scale().scale(tick)
133 | const size = averageCharSize().x * localProps.format(tick).length
134 | const start = tickPosition - size / 2
135 | const end = tickPosition + size / 2
136 |
137 | if (start < 0) {
138 | return tickPosition - start
139 | }
140 | if (end > chartContext.width()) {
141 | return tickPosition - (end - chartContext.width())
142 | }
143 | return tickPosition
144 | }
145 | case 'left':
146 | return chartContext.getInset('left')
147 | case 'right':
148 | return chartContext.width() - chartContext.getInset('right')
149 | }
150 | }
151 |
152 | const y = (tick: any) => {
153 | switch (axisContext.position()) {
154 | case 'top':
155 | return chartContext.getInset('top', 'axis.label')
156 | case 'bottom':
157 | return (
158 | chartContext.height() - chartContext.getInset('bottom', 'axis.label')
159 | )
160 | case 'left':
161 | case 'right':
162 | return axisContext.scale().scale(tick)
163 | }
164 | }
165 |
166 | return (
167 |
168 |
169 | {(tick) => (
170 |
171 | {localProps.format(tick)}
172 |
173 | )}
174 |
175 |
176 | )
177 | }
178 |
179 | export default Label
180 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Line.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import { type ComponentProps, mergeProps } from 'solid-js'
4 |
5 | export type LineProps = Omit, 'x1' | 'y1' | 'x2' | 'y2'>
6 |
7 | /** Baseline for the axis.
8 | *
9 | * @data `data-sc-axis-line` - Present on every axis line element.
10 | */
11 | const Line = (props: LineProps) => {
12 | const defaultedProps = mergeProps(
13 | {
14 | stroke: 'currentColor',
15 | 'stroke-width': 1,
16 | },
17 | props,
18 | )
19 |
20 | const chartContext = useChartContext()
21 | const axisContext = useAxisContext()
22 |
23 | const x = () => {
24 | switch (axisContext.position()) {
25 | case 'top':
26 | case 'bottom':
27 | return chartContext.getInset('left')
28 | case 'left':
29 | return chartContext.getInset('left')
30 | case 'right':
31 | return chartContext.width() - chartContext.getInset('right')
32 | }
33 | }
34 |
35 | const y = () => {
36 | switch (axisContext.position()) {
37 | case 'top':
38 | return chartContext.getInset('top')
39 | case 'bottom':
40 | return chartContext.height() - chartContext.getInset('bottom')
41 | case 'left':
42 | case 'right':
43 | return chartContext.getInset('top')
44 | }
45 | }
46 |
47 | const x2 = () => {
48 | switch (axisContext.position()) {
49 | case 'top':
50 | case 'bottom':
51 | return chartContext.width() - chartContext.getInset('right')
52 | case 'left':
53 | return chartContext.getInset('left')
54 | case 'right':
55 | return chartContext.width() - chartContext.getInset('right')
56 | }
57 | }
58 |
59 | const y2 = () => {
60 | switch (axisContext.position()) {
61 | case 'top':
62 | return chartContext.getInset('top')
63 | case 'bottom':
64 | return chartContext.height() - chartContext.getInset('bottom')
65 | case 'left':
66 | case 'right':
67 | return chartContext.height() - chartContext.getInset('bottom')
68 | }
69 | }
70 |
71 | return (
72 |
80 | )
81 | }
82 |
83 | export default Line
84 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Mark.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import type { OverrideProps } from '@src/lib/types'
4 | import { type ComponentProps, For, mergeProps } from 'solid-js'
5 |
6 | export type MarkProps = OverrideProps<
7 | Omit, 'x1' | 'y1' | 'x2' | 'y2'>,
8 | {
9 | /**
10 | * Length of the mark lines in px.
11 | * @defaultValue 6px
12 | * */
13 | length?: number
14 | }
15 | >
16 |
17 | /** Mark lines rendered between the chart and axis labels.
18 | *
19 | * @data `data-sc-axis-mark-group` - Present on every mark group element.
20 | * @data `data-sc-axis-mark` - Present on every mark line element.
21 | */
22 | const Mark = (props: MarkProps) => {
23 | const defaultedProps = mergeProps(
24 | {
25 | stroke: 'currentColor',
26 | 'stroke-width': 1,
27 | length: 6,
28 | },
29 | props,
30 | )
31 |
32 | const chartContext = useChartContext()
33 | const axisContext = useAxisContext()
34 |
35 | const x = (tick: any) => {
36 | switch (axisContext.position()) {
37 | case 'top':
38 | case 'bottom':
39 | return axisContext.scale().scale(tick)
40 | case 'left':
41 | return chartContext.getInset('left')
42 | case 'right':
43 | return chartContext.width() - chartContext.getInset('right')
44 | }
45 | }
46 |
47 | const y = (tick: any) => {
48 | switch (axisContext.position()) {
49 | case 'top':
50 | return chartContext.getInset('top')
51 | case 'bottom':
52 | return chartContext.height() - chartContext.getInset('bottom')
53 | case 'left':
54 | case 'right':
55 | return axisContext.scale().scale(tick)
56 | }
57 | }
58 |
59 | const x2 = (tick: any) => {
60 | switch (axisContext.position()) {
61 | case 'top':
62 | case 'bottom':
63 | return axisContext.scale().scale(tick)
64 | case 'left':
65 | return chartContext.getInset('left') - defaultedProps.length
66 | case 'right':
67 | return (
68 | chartContext.width() -
69 | chartContext.getInset('right') +
70 | defaultedProps.length
71 | )
72 | }
73 | }
74 |
75 | const y2 = (tick: any) => {
76 | switch (axisContext.position()) {
77 | case 'top':
78 | return chartContext.getInset('top') - defaultedProps.length
79 | case 'bottom':
80 | return (
81 | chartContext.height() -
82 | chartContext.getInset('bottom') +
83 | defaultedProps.length
84 | )
85 | case 'left':
86 | case 'right':
87 | return axisContext.scale().scale(tick)
88 | }
89 | }
90 |
91 | return (
92 |
93 |
94 | {(tick) => (
95 |
103 | )}
104 |
105 |
106 | )
107 | }
108 |
109 | export default Mark
110 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { combineStyle } from '@corvu/utils/dom'
2 | import { useAxisContext } from '@src/axis/context'
3 | import { useChartContext } from '@src/components/context'
4 | import createClosestTick from '@src/lib/createClosestTick'
5 | import createSize from '@src/lib/dom/createSize'
6 | import type { OverrideProps } from '@src/lib/types'
7 | import {
8 | type ComponentProps,
9 | type JSX,
10 | createMemo,
11 | createSignal,
12 | mergeProps,
13 | splitProps,
14 | } from 'solid-js'
15 | import { Portal, isDev } from 'solid-js/web'
16 |
17 | export type TooltipProps = OverrideProps<
18 | ComponentProps<'div'>,
19 | {
20 | /**
21 | * The gap between the tick and the tooltip.
22 | * @defaultValue 16px
23 | */
24 | tickGap?: number
25 | /**
26 | * The gap between the pointer and the tooltip.
27 | * @defaultValue 16px
28 | */
29 | pointerGap?: number
30 | /**
31 | * Accepts a function as it's children that receives the data of the closest tick.
32 | */
33 | children: (props: { data: any }) => JSX.Element
34 | }
35 | >
36 |
37 | /** Interactive tooltip component that renders near the closest tick.
38 | *
39 | * @data `data-sc-axis-tooltip` - Present on every tooltip element.
40 | */
41 | const Tooltip = (props: TooltipProps) => {
42 | const defaultedProps = mergeProps(
43 | {
44 | tickGap: 16,
45 | pointerGap: 16,
46 | },
47 | props,
48 | )
49 | const [localProps, otherProps] = splitProps(defaultedProps, [
50 | 'tickGap',
51 | 'pointerGap',
52 | 'children',
53 | 'style',
54 | ])
55 | const chartContext = useChartContext()
56 | const axisContext = useAxisContext()
57 |
58 | if (isDev && axisContext.scale().type === 'linear') {
59 | throw new Error(
60 | '[solid-charts] Tooltip can not be used with an axis of type linear',
61 | )
62 | }
63 |
64 | const [tooltipRef, setTooltipRef] = createSignal(null)
65 |
66 | const tooltipSize = createSize({
67 | element: tooltipRef,
68 | })
69 |
70 | const pointerPosition = createMemo<{ x: number; y: number } | undefined>(
71 | (prev) => {
72 | const pointerPosition = chartContext.pointerPosition()
73 | if (!pointerPosition) return prev
74 | return pointerPosition
75 | },
76 | )
77 |
78 | const closestTick = createClosestTick({
79 | axis: axisContext.axis,
80 | chartContext,
81 | })
82 |
83 | const x = () => {
84 | const _pointerPosition = pointerPosition()
85 | const tick = closestTick()
86 | if (!_pointerPosition || !tick) return 0
87 |
88 | switch (axisContext.axis()) {
89 | case 'x': {
90 | const tickPosition = chartContext.toContainerPosition(
91 | tick.position,
92 | 'width',
93 | )
94 | const preferredPosition = tickPosition + localProps.tickGap
95 | const _tooltipSize = tooltipSize()
96 | if (
97 | _tooltipSize &&
98 | preferredPosition + _tooltipSize[0] >
99 | chartContext.toContainerPosition(chartContext.width(), 'width')
100 | ) {
101 | return tickPosition - localProps.tickGap - _tooltipSize[0]
102 | }
103 | return preferredPosition
104 | }
105 | case 'y': {
106 | const preferredPosition = _pointerPosition.x + localProps.pointerGap
107 | const _tooltipSize = tooltipSize()
108 | if (
109 | _tooltipSize &&
110 | preferredPosition + _tooltipSize[0] >
111 | chartContext.toContainerPosition(chartContext.width(), 'width')
112 | ) {
113 | return _pointerPosition.x - localProps.pointerGap - _tooltipSize[0]
114 | }
115 | return preferredPosition
116 | }
117 | }
118 | }
119 |
120 | const y = () => {
121 | const _pointerPosition = pointerPosition()
122 | const tick = closestTick()
123 | if (!_pointerPosition || !tick) return 0
124 |
125 | switch (axisContext.axis()) {
126 | case 'x': {
127 | const preferredPosition = _pointerPosition.y + localProps.pointerGap
128 | const _tooltipSize = tooltipSize()
129 | if (
130 | _tooltipSize &&
131 | preferredPosition + _tooltipSize[1] >
132 | chartContext.toContainerPosition(chartContext.height(), 'height')
133 | ) {
134 | return _pointerPosition.y! - localProps.pointerGap - _tooltipSize[1]
135 | }
136 | return preferredPosition
137 | }
138 | case 'y': {
139 | const tickPosition = chartContext.toContainerPosition(
140 | tick.position,
141 | 'height',
142 | )
143 | const preferredPosition = tickPosition + localProps.tickGap
144 | const _tooltipSize = tooltipSize()
145 | if (
146 | _tooltipSize &&
147 | preferredPosition + _tooltipSize[1] >
148 | chartContext.toContainerPosition(chartContext.height(), 'height')
149 | ) {
150 | return tickPosition - localProps.tickGap - _tooltipSize[1]
151 | }
152 | return preferredPosition
153 | }
154 | }
155 | }
156 |
157 | return (
158 |
159 |
175 | {localProps.children({
176 | get data() {
177 | return chartContext.data()[closestTick()?.index ?? 0]
178 | },
179 | })}
180 |
181 |
182 | )
183 | }
184 |
185 | export default Tooltip
186 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/ValueLine.tsx:
--------------------------------------------------------------------------------
1 | import { useAxisContext } from '@src/axis/context'
2 | import { useChartContext } from '@src/components/context'
3 | import type { OverrideProps } from '@src/lib/types'
4 | import { type ComponentProps, mergeProps, splitProps } from 'solid-js'
5 |
6 | export type ValueLineProps = OverrideProps<
7 | Omit, 'x1' | 'y1' | 'x2' | 'y2'>,
8 | {
9 | /**
10 | * The value in the axis scale to draw the line at.
11 | */
12 | value: any
13 | }
14 | >
15 |
16 | /** Line rendered at a specific value in the axis scale.
17 | *
18 | * @data `data-sc-axis-value-line` - Present on every value line element.
19 | */
20 | const ValueLine = (props: ValueLineProps) => {
21 | const defaultedProps = mergeProps(
22 | {
23 | stroke: 'currentColor',
24 | 'stroke-width': 1,
25 | },
26 | props,
27 | )
28 | const [localProps, otherProps] = splitProps(defaultedProps, ['value'])
29 |
30 | const chartContext = useChartContext()
31 | const axisContext = useAxisContext()
32 |
33 | const x = () => {
34 | switch (axisContext.axis()) {
35 | case 'x':
36 | return axisContext.scale().scale(localProps.value)
37 | case 'y':
38 | return chartContext.getInset('left')
39 | }
40 | }
41 |
42 | const y = () => {
43 | switch (axisContext.axis()) {
44 | case 'x':
45 | return chartContext.getInset('top')
46 | case 'y':
47 | return axisContext.scale().scale(localProps.value)
48 | }
49 | }
50 |
51 | const x2 = () => {
52 | switch (axisContext.axis()) {
53 | case 'x':
54 | return axisContext.scale().scale(localProps.value)
55 | case 'y':
56 | return chartContext.width() - chartContext.getInset('right')
57 | }
58 | }
59 |
60 | const y2 = () => {
61 | switch (axisContext.axis()) {
62 | case 'x':
63 | return chartContext.height() - chartContext.getInset('bottom')
64 | case 'y':
65 | return axisContext.scale().scale(localProps.value)
66 | }
67 | }
68 |
69 | return (
70 |
79 | )
80 | }
81 |
82 | export default ValueLine
83 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/axis/context.ts:
--------------------------------------------------------------------------------
1 | import type { Scale } from '@src/lib/createScale'
2 | import { type Accessor, createContext, useContext } from 'solid-js'
3 |
4 | export type AxisContextType = {
5 | scale: Accessor
6 | ticks: Accessor
7 | axis: Accessor<'x' | 'y'>
8 | position: Accessor<'top' | 'right' | 'bottom' | 'left'>
9 | labelTicks: Accessor
10 | setLabelTicks: (ticks: any[]) => void
11 | }
12 |
13 | export const AxisContext = createContext()
14 |
15 | export const useAxisContext = () => {
16 | const context = useContext(AxisContext)
17 | if (!context) {
18 | throw new Error(
19 | '[solid-charts]: Axis context not found. Make sure to wrap Axis components in ',
20 | )
21 | }
22 | return context
23 | }
24 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/components/context.ts:
--------------------------------------------------------------------------------
1 | import { type Accessor, createContext, useContext } from 'solid-js'
2 |
3 | export type Inset = {
4 | top: Map
5 | right: Map
6 | bottom: Map
7 | left: Map
8 | }
9 |
10 | export type BarConfig = {
11 | bandGap: number | `${number}%`
12 | barGap: number | `${number}%`
13 | barSize?: number | `${number}%`
14 | maxBarSize?: number | `${number}%`
15 | }
16 |
17 | export type ChartContextType = {
18 | data: Accessor
19 | width: Accessor
20 | height: Accessor
21 | getInset: (
22 | edge: 'top' | 'right' | 'bottom' | 'left',
23 | exclude?: string,
24 | ) => number
25 | registerInset: (
26 | edge: 'top' | 'right' | 'bottom' | 'left',
27 | key: string,
28 | value: number,
29 | ) => void
30 | unregisterInset: (
31 | edge: 'top' | 'right' | 'bottom' | 'left',
32 | key: string,
33 | ) => void
34 | pointerPosition: Accessor<{ x: number; y: number } | null>
35 | pointerInChart: Accessor
36 | wrapperRef: Accessor
37 | stacks: Accessor<
38 | Map<
39 | string,
40 | Map<
41 | string,
42 | {
43 | seriesIds: Set
44 | values: number[]
45 | }
46 | >
47 | >
48 | >
49 | registerStack: (
50 | stackId: string,
51 | dataKey: string,
52 | seriesId: string,
53 | values: number[],
54 | ) => void
55 | unregisterStack: (stackId: string, dataKey: string, seriesId: string) => void
56 | registerAxis: (
57 | axisId: string,
58 | data:
59 | | { type: 'series'; seriesId: string; min: number; max: number }
60 | | {
61 | type: 'user'
62 | range: [number | 'min', number | 'max']
63 | },
64 | ) => void
65 | unregisterAxis: (
66 | axisId: string,
67 | data:
68 | | {
69 | type: 'series'
70 | seriesId: string
71 | }
72 | | {
73 | type: 'user'
74 | },
75 | ) => void
76 | getAxis: (axisId: string) => {
77 | min: number
78 | max: number
79 | userDefined: boolean
80 | }
81 | barConfig: Accessor
82 | bars: Accessor>
83 | registerBar: (key: string) => void
84 | unregisterBar: (key: string) => void
85 | toSvgPosition: (position: number, dimension: 'width' | 'height') => number
86 | toContainerPosition: (
87 | position: number,
88 | dimension: 'width' | 'height',
89 | ) => number
90 | }
91 |
92 | export const ChartContext = createContext()
93 |
94 | export const useChartContext = () => {
95 | const context = useContext(ChartContext)
96 | if (!context) {
97 | throw new Error(
98 | '[solid-charts]: Chart context not found. Make sure to wrap chart components in ',
99 | )
100 | }
101 | return context
102 | }
103 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/curves.ts:
--------------------------------------------------------------------------------
1 | import {
2 | curveBasis,
3 | curveBasisClosed,
4 | curveBasisOpen,
5 | curveBumpX,
6 | curveBumpY,
7 | curveCardinal,
8 | curveCardinalClosed,
9 | curveCardinalOpen,
10 | curveCatmullRom,
11 | curveCatmullRomClosed,
12 | curveCatmullRomOpen,
13 | curveLinear,
14 | curveLinearClosed,
15 | curveMonotoneX,
16 | curveMonotoneY,
17 | curveNatural,
18 | curveStep,
19 | curveStepAfter,
20 | curveStepBefore,
21 | } from 'd3-shape'
22 |
23 | export {
24 | curveBasis,
25 | curveBasisClosed,
26 | curveBasisOpen,
27 | curveBumpX,
28 | curveBumpY,
29 | curveCardinal,
30 | curveCardinalClosed,
31 | curveCardinalOpen,
32 | curveCatmullRom,
33 | curveCatmullRomClosed,
34 | curveCatmullRomOpen,
35 | curveLinear,
36 | curveLinearClosed,
37 | curveMonotoneX,
38 | curveMonotoneY,
39 | curveNatural,
40 | curveStep,
41 | curveStepAfter,
42 | curveStepBefore,
43 | }
44 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/index.ts:
--------------------------------------------------------------------------------
1 | import Axis, {
2 | type AxisProps,
3 | type XAxisProps,
4 | type YAxisProps,
5 | } from '@src/axis/Axis'
6 | import AxisCursor, {
7 | type CursorProps as AxisCursorProps,
8 | } from '@src/axis/Cursor'
9 | import AxisGrid, { type GridProps as AxisGridProps } from '@src/axis/Grid'
10 | import AxisLabel, { type LabelProps as AxisLabelProps } from '@src/axis/Label'
11 | import AxisLine, { type LineProps as AxisLineProps } from '@src/axis/Line'
12 | import AxisMark, { type MarkProps as AxisMarkProps } from '@src/axis/Mark'
13 | import AxisTooltip, {
14 | type TooltipProps as AxisTooltipProps,
15 | } from '@src/axis/Tooltip'
16 | import AxisValueLine, {
17 | type ValueLineProps as AxisValueLineProps,
18 | } from '@src/axis/ValueLine'
19 | import Chart, { type ChartProps } from '@src/components/Chart'
20 | import type { BarConfig } from '@src/components/context'
21 | import type { Scale, ScaleType } from '@src/lib/createScale'
22 | import type { OverrideProps } from '@src/lib/types'
23 | import Area, { type AreaProps } from '@src/series/Area'
24 | import Bar, { type BarProps } from '@src/series/Bar'
25 | import Line, { type LineProps } from '@src/series/Line'
26 | import Point, { type PointProps } from '@src/series/Point'
27 | import type { CurveProps } from '@src/shapes/Curve'
28 |
29 | export type {
30 | // Chart
31 | ChartProps,
32 | BarConfig,
33 | // Series
34 | AreaProps,
35 | BarProps,
36 | LineProps,
37 | PointProps,
38 | // Axis
39 | AxisProps,
40 | XAxisProps,
41 | YAxisProps,
42 | AxisCursorProps,
43 | AxisGridProps,
44 | AxisLabelProps,
45 | AxisLineProps,
46 | AxisMarkProps,
47 | AxisTooltipProps,
48 | AxisValueLineProps,
49 | // Others
50 | OverrideProps,
51 | CurveProps,
52 | ScaleType,
53 | Scale,
54 | }
55 |
56 | export {
57 | Chart,
58 | // Series
59 | Area,
60 | Bar,
61 | Line,
62 | Point,
63 | // Axis
64 | Axis,
65 | AxisCursor,
66 | AxisGrid,
67 | AxisLabel,
68 | AxisLine,
69 | AxisMark,
70 | AxisTooltip,
71 | AxisValueLine,
72 | }
73 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createBands.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { gapToPadding } from '@src/lib/utils'
3 | import { scaleBand } from 'd3-scale'
4 | import { type Accessor, createMemo } from 'solid-js'
5 |
6 | const createBands = (props: {
7 | seriesId: string
8 | stackId: Accessor
9 | data: Accessor
10 | chartContext: ChartContextType
11 | }) => {
12 | return createMemo(() => {
13 | const data = props.data()
14 |
15 | const left = props.chartContext.getInset('left')
16 | const right =
17 | props.chartContext.width() - props.chartContext.getInset('right')
18 |
19 | const chartWidth = right - left
20 |
21 | const barConfig = props.chartContext.barConfig()
22 | const bandGap = gapToPadding(barConfig.bandGap, chartWidth / data.length)
23 | const barGap = gapToPadding(barConfig.barGap, chartWidth / data.length)
24 |
25 | const bandScale = scaleBand()
26 | .domain(Array(data.length).keys().map(String))
27 | .range([left, right])
28 | .paddingInner(bandGap)
29 |
30 | const bars = props.chartContext.bars()
31 | const barGroupScale = scaleBand()
32 | .domain([...bars.values()])
33 | .range([0, bandScale.bandwidth()])
34 | .paddingInner(barGap)
35 |
36 | const barWidth = barGroupScale.bandwidth()
37 |
38 | return Array(data.length)
39 | .fill(null)
40 | .map((_, index) => {
41 | const bandX = bandScale(String(index))!
42 | const barGroupX = barGroupScale(
43 | String(props.stackId() ?? props.seriesId),
44 | )!
45 |
46 | return {
47 | x: bandX + barGroupX,
48 | width: barWidth,
49 | }
50 | })
51 | })
52 | }
53 |
54 | export default createBands
55 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createBaseLine.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { scaleLinear } from 'd3-scale'
3 | import { type Accessor, createMemo } from 'solid-js'
4 |
5 | /**
6 | * Calculates the baseline coordinates based on the axis and stack that the series belongs to.
7 | */
8 | const createBaseLine = (props: {
9 | dataKey: Accessor
10 | axisId: Accessor
11 | stackId: Accessor
12 | data: Accessor
13 | chartContext: ChartContextType
14 | }) => {
15 | return createMemo(() => {
16 | const top = props.chartContext.getInset('top')
17 | const bottom =
18 | props.chartContext.height() - props.chartContext.getInset('bottom')
19 |
20 | const axis = props.chartContext.getAxis(props.axisId())
21 | let domainMin = axis.min
22 | if (!axis.userDefined) {
23 | domainMin = Math.min(axis.min, 0)
24 | }
25 | let scaleHeight = scaleLinear([domainMin, axis.max], [bottom, top])
26 | if (!axis.userDefined) {
27 | scaleHeight = scaleHeight.nice()
28 | }
29 |
30 | const stackId = props.stackId()
31 | const stack =
32 | stackId !== undefined && props.chartContext.stacks().get(stackId)
33 |
34 | if (!stack) return scaleHeight(0)
35 |
36 | const stackDataKeys = [...stack.keys()]
37 | const thisSeriesIdx = stackDataKeys.indexOf(props.dataKey() ?? '')
38 |
39 | if (thisSeriesIdx <= 0) return scaleHeight(0)
40 |
41 | return Array(props.data().length)
42 | .fill(null)
43 | .map((_, dataIdx) => {
44 | let baseLine = 0
45 |
46 | for (let seriesIdx = 0; seriesIdx < thisSeriesIdx; seriesIdx++) {
47 | baseLine += stack.get(stackDataKeys[seriesIdx]!)?.values[dataIdx] ?? 0
48 | }
49 |
50 | return scaleHeight(baseLine)
51 | })
52 | })
53 | }
54 |
55 | export default createBaseLine
56 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createClosestTick.ts:
--------------------------------------------------------------------------------
1 | import { type ChartContextType, useChartContext } from '@src/components/context'
2 | import { getBarPadding } from '@src/lib/utils'
3 | import { scaleLinear } from 'd3-scale'
4 | import { type Accessor, createMemo } from 'solid-js'
5 |
6 | type ClosestTick = {
7 | index: number
8 | position: number
9 | }
10 |
11 | const createClosestTick = (props: {
12 | axis: Accessor<'x' | 'y'>
13 | chartContext: ChartContextType
14 | }) => {
15 | const chartContext = useChartContext()
16 |
17 | return createMemo((prev) => {
18 | const pointerPosition = chartContext.pointerPosition()
19 | if (!pointerPosition) return prev
20 |
21 | let position: number
22 | let start: number
23 | let end: number
24 |
25 | switch (props.axis()) {
26 | case 'x': {
27 | const barPadding = getBarPadding(props.chartContext)
28 | position = chartContext.toSvgPosition(pointerPosition.x, 'width')
29 | start = chartContext.getInset('left') + barPadding
30 | end = chartContext.width() - chartContext.getInset('right') - barPadding
31 | break
32 | }
33 | case 'y': {
34 | position = chartContext.toSvgPosition(pointerPosition.y, 'height')
35 | start = chartContext.height() - chartContext.getInset('bottom')
36 | end = chartContext.getInset('top')
37 | break
38 | }
39 | }
40 |
41 | const scale = scaleLinear(
42 | [start, end],
43 | [0, chartContext.data().length - 1],
44 | ).clamp(true)
45 |
46 | const tickIndex = Math.round(scale(position))
47 |
48 | if (tickIndex === prev?.index) return prev
49 |
50 | return {
51 | index: tickIndex,
52 | position: scale.invert(tickIndex),
53 | }
54 | })
55 | }
56 |
57 | export default createClosestTick
58 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createLabelTicks.ts:
--------------------------------------------------------------------------------
1 | import type { AxisContextType } from '@src/axis/context'
2 | import type { ChartContextType } from '@src/components/context'
3 | import type { Scale } from '@src/lib/createScale'
4 | import { type Accessor, createEffect } from 'solid-js'
5 |
6 | const createLabelTicks = (props: {
7 | ticks: Accessor
8 | interval: Accessor<
9 | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd' | number
10 | >
11 | labelGap: Accessor
12 | format: Accessor<(value: any) => string>
13 | averageCharSize: Accessor<{ x: number; y: number }>
14 | chartContext: ChartContextType
15 | axisContext: AxisContextType
16 | }) => {
17 | createEffect(() => {
18 | const scale = props.axisContext.scale()
19 |
20 | const interval = props.interval()
21 | if (typeof interval === 'number') {
22 | props.axisContext.setLabelTicks(
23 | props.ticks().filter((_, i) => i % interval === 0),
24 | )
25 | return
26 | }
27 |
28 | const axis = props.axisContext.axis()
29 | const chartSize =
30 | axis === 'x' ? props.chartContext.width() : props.chartContext.height()
31 |
32 | switch (interval) {
33 | case 'preserveStart': {
34 | const sign = axis === 'x' ? 1 : -1
35 | const visibleLabels = calculateLabelTicks(
36 | props.ticks(),
37 | sign,
38 | chartSize,
39 | props.averageCharSize(),
40 | axis,
41 | props.labelGap(),
42 | props.format(),
43 | scale.scale,
44 | )
45 | props.axisContext.setLabelTicks(visibleLabels)
46 | break
47 | }
48 | case 'preserveEnd': {
49 | const sign = axis === 'x' ? -1 : 1
50 | const visibleLabels = calculateLabelTicks(
51 | [...props.ticks()].reverse(),
52 | sign,
53 | chartSize,
54 | props.averageCharSize(),
55 | axis,
56 | props.labelGap(),
57 | props.format(),
58 | scale.scale,
59 | )
60 | props.axisContext.setLabelTicks(visibleLabels.reverse())
61 | break
62 | }
63 | case 'preserveStartEnd': {
64 | const sign = axis === 'x' ? -1 : 1
65 | const ticks = [...props.ticks()].reverse()
66 | const firstTick = ticks[ticks.length - 1]!
67 | const size =
68 | props.averageCharSize()[axis] *
69 | (axis === 'x' ? props.format()(firstTick).length : 1)
70 |
71 | const tickPosition = scale.scale(firstTick)!
72 | const start = tickPosition - size / 2
73 | let end = tickPosition + size / 2
74 | if (axis === 'x' && start < 0) {
75 | end = size
76 | }
77 |
78 | const visibleLabels = calculateLabelTicks(
79 | ticks.slice(0, -1),
80 | sign,
81 | chartSize,
82 | props.averageCharSize(),
83 | axis,
84 | props.labelGap(),
85 | props.format(),
86 | scale.scale,
87 | end + props.labelGap(),
88 | )
89 | props.axisContext.setLabelTicks([...visibleLabels, firstTick].reverse())
90 | break
91 | }
92 | }
93 | })
94 | }
95 |
96 | const calculateLabelTicks = (
97 | ticks: any[],
98 | sign: number,
99 | chartSize: number,
100 | averageCharSize: { x: number; y: number },
101 | axis: 'x' | 'y',
102 | labelGap: number,
103 | format: (value: any) => string,
104 | scale: Scale['scale'],
105 | fixedStart?: number,
106 | ) => {
107 | const visibleLabels = []
108 | let lastPosition =
109 | sign > 0 ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY
110 |
111 | for (const tick of ticks) {
112 | const size =
113 | averageCharSize[axis] * (axis === 'x' ? format(tick).length : 1)
114 |
115 | const tickPosition = scale(tick)!
116 | let start = tickPosition - size / 2
117 | let end = tickPosition + size / 2
118 |
119 | if (axis === 'x') {
120 | if (start < 0) {
121 | start = 0
122 | end = start + size
123 | } else if (end > chartSize) {
124 | end = chartSize
125 | start = end - size
126 | }
127 | }
128 |
129 | const hasGap =
130 | sign > 0
131 | ? start >= lastPosition + labelGap
132 | : end <= lastPosition - labelGap
133 |
134 | if ((!fixedStart || start >= fixedStart) && hasGap && end <= chartSize) {
135 | visibleLabels.push(tick)
136 | lastPosition = sign > 0 ? end : start
137 | }
138 | }
139 | return visibleLabels
140 | }
141 |
142 | export default createLabelTicks
143 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createPoints.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { getBarPadding } from '@src/lib/utils'
3 | import { scaleLinear } from 'd3-scale'
4 | import { type Accessor, createMemo } from 'solid-js'
5 |
6 | const createPoints = (props: {
7 | dataKey: Accessor
8 | axisId: Accessor
9 | stackId: Accessor
10 | data: Accessor
11 | chartContext: ChartContextType
12 | }) => {
13 | return createMemo(() => {
14 | const data = props.data()
15 |
16 | const barPadding = getBarPadding(props.chartContext)
17 | const left = props.chartContext.getInset('left') + barPadding
18 | const right =
19 | props.chartContext.width() -
20 | props.chartContext.getInset('right') -
21 | barPadding
22 | const scaleWidth = scaleLinear([0, data.length - 1], [left, right])
23 |
24 | const top = props.chartContext.getInset('top')
25 | const bottom =
26 | props.chartContext.height() - props.chartContext.getInset('bottom')
27 |
28 | const axis = props.chartContext.getAxis(props.axisId())
29 | let domainMin = axis.min
30 | if (!axis.userDefined) {
31 | domainMin = Math.min(axis.min, 0)
32 | }
33 | let scaleHeight = scaleLinear([domainMin, axis.max], [bottom, top])
34 | if (!axis.userDefined) {
35 | scaleHeight = scaleHeight.nice()
36 | }
37 |
38 | const stackId = props.stackId()
39 | const stack =
40 | stackId !== undefined && props.chartContext.stacks().get(stackId)
41 | return data.map((value, dataIdx) => {
42 | let stackedValue = value
43 |
44 | if (stack) {
45 | const stackDataKeys = [...stack.keys()]
46 | const thisSeriesIdx = stackDataKeys.indexOf(props.dataKey() ?? '')
47 |
48 | if (thisSeriesIdx > 0) {
49 | for (let seriesIdx = 0; seriesIdx < thisSeriesIdx; seriesIdx++) {
50 | stackedValue +=
51 | stack.get(stackDataKeys[seriesIdx]!)?.values[dataIdx] || 0
52 | }
53 | }
54 | }
55 |
56 | return [scaleWidth(dataIdx), scaleHeight(stackedValue)] as [
57 | number,
58 | number,
59 | ]
60 | })
61 | })
62 | }
63 |
64 | export default createPoints
65 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createScale.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { getBarPadding } from '@src/lib/utils'
3 | import {
4 | type ScaleLinear,
5 | type ScalePoint,
6 | scaleLinear,
7 | scalePoint,
8 | } from 'd3-scale'
9 | import { type Accessor, createMemo } from 'solid-js'
10 |
11 | export type ScaleType = 'linear' | 'categorial'
12 | export type Scale =
13 | | {
14 | type: 'linear'
15 | scale: ScaleLinear
16 | }
17 | | {
18 | type: 'categorial'
19 | scale: ScalePoint
20 | }
21 |
22 | const createScale = (props: {
23 | axis: Accessor<'x' | 'y'>
24 | type: Accessor
25 | axisId: Accessor
26 | data: Accessor
27 | chartContext: ChartContextType
28 | }) => {
29 | return createMemo(() => {
30 | let start: number
31 | let end: number
32 | switch (props.axis()) {
33 | case 'x': {
34 | const barPadding = getBarPadding(props.chartContext)
35 | start = props.chartContext.getInset('left') + barPadding
36 | end =
37 | props.chartContext.width() -
38 | props.chartContext.getInset('right') -
39 | barPadding
40 | break
41 | }
42 | case 'y': {
43 | start =
44 | props.chartContext.height() - props.chartContext.getInset('bottom')
45 | end = props.chartContext.getInset('top')
46 | break
47 | }
48 | }
49 |
50 | switch (props.type()) {
51 | case 'linear': {
52 | const axis = props.chartContext.getAxis(props.axisId())
53 | let domainMin = axis.min
54 | if (!axis.userDefined) {
55 | domainMin = Math.min(axis.min, 0)
56 | }
57 | let scale = scaleLinear([domainMin, axis.max], [start, end])
58 | if (!axis.userDefined) {
59 | scale = scale.nice()
60 | }
61 |
62 | return {
63 | type: 'linear' as const,
64 | scale,
65 | }
66 | }
67 | case 'categorial':
68 | return {
69 | type: 'categorial' as const,
70 | scale: scalePoint(props.data(), [start, end]),
71 | }
72 | }
73 | })
74 | }
75 |
76 | export default createScale
77 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createSeries.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { type Accessor, createEffect, onCleanup } from 'solid-js'
3 |
4 | const createSeries = (props: {
5 | seriesId: string
6 | dataKey: Accessor
7 | axisId: Accessor
8 | stackId: Accessor
9 | data: Accessor
10 | chartContext: ChartContextType
11 | }) => {
12 | createEffect(() => {
13 | const stackId = props.stackId()
14 | if (!stackId) return
15 |
16 | props.chartContext.registerStack(
17 | stackId,
18 | props.dataKey() ?? '',
19 | props.seriesId,
20 | props.data(),
21 | )
22 | onCleanup(() =>
23 | props.chartContext.unregisterStack(
24 | stackId,
25 | props.dataKey() ?? '',
26 | props.seriesId,
27 | ),
28 | )
29 | })
30 |
31 | createEffect(() => {
32 | const stackId = props.stackId()
33 | const stack =
34 | stackId !== undefined && props.chartContext.stacks().get(stackId)
35 |
36 | const data = props.data()
37 |
38 | const stackValues = stack
39 | ? [...stack.values()].flatMap((stack) => stack.values)
40 | : []
41 | const min = Math.min(...data, ...stackValues)
42 |
43 | let max: number | null = null
44 | if (stack) {
45 | const stackDataKeys = [...stack.keys()]
46 |
47 | for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
48 | let stackedValue = 0
49 |
50 | for (const stackDataKey of stackDataKeys) {
51 | stackedValue += stack.get(stackDataKey)?.values[dataIdx] ?? 0
52 | }
53 |
54 | max = Math.max(max ?? stackedValue, stackedValue)
55 | }
56 | }
57 | max = max ?? Math.max(...data)
58 |
59 | props.chartContext.registerAxis(props.axisId(), {
60 | type: 'series',
61 | seriesId: props.seriesId,
62 | min,
63 | max,
64 | })
65 | onCleanup(() =>
66 | props.chartContext.unregisterAxis(props.axisId(), {
67 | type: 'series',
68 | seriesId: props.seriesId,
69 | }),
70 | )
71 | })
72 | }
73 |
74 | export default createSeries
75 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/createTicks.ts:
--------------------------------------------------------------------------------
1 | import type { Scale } from '@src/lib/createScale'
2 | import { type Accessor, createMemo } from 'solid-js'
3 |
4 | const createTicks = (props: {
5 | scale: Accessor
6 | tickCount: Accessor
7 | tickValues: Accessor
8 | }) => {
9 | return createMemo(() => {
10 | const tickValues = props.tickValues()
11 | if (tickValues) return tickValues
12 |
13 | const scale = props.scale()
14 | switch (scale.type) {
15 | case 'linear':
16 | return scale.scale.ticks(props.tickCount())
17 | case 'categorial': {
18 | return scale.scale.domain()
19 | }
20 | }
21 | })
22 | }
23 |
24 | export default createTicks
25 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/dom/charSize.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentProps } from 'solid-js'
2 | import { assign } from 'solid-js/web'
3 |
4 | const ALPHABET = 'abcdefghijklmnopqrstuvwxyz '
5 |
6 | const PROPS_IGNORELIST: (keyof Omit, 'x' | 'y'>)[] = [
7 | 'fill',
8 | 'text-anchor',
9 | 'dominant-baseline',
10 | 'dx',
11 | 'dy',
12 | ]
13 |
14 | type SizeCache = Map
15 |
16 | const sizeCache: SizeCache = new Map()
17 |
18 | /**
19 | * Gets the average size of a character by creating a temporary, hidden text element
20 | * Uses the same properties as the label text element and inserts it under the same parent
21 | * @param parentRef The parent element
22 | * @param props The properties to assign to the text element
23 | * @returns x and y size of the average character
24 | * ```typescript
25 | * { x: number, y: number }
26 | * ```
27 | */
28 | const getAverageCharSize = (
29 | parentRef: SVGElement,
30 | props: Omit, 'x' | 'y'>,
31 | axisId: string,
32 | ) => {
33 | const propsCopy = { ...props }
34 | for (const prop of PROPS_IGNORELIST) {
35 | delete propsCopy[prop]
36 | }
37 |
38 | const cacheKey = `${axisId}-${JSON.stringify(propsCopy)}`
39 | const cachedSize = sizeCache.get(cacheKey)
40 | if (cachedSize) return cachedSize
41 |
42 | const textElement = parentRef.ownerDocument.createElementNS(
43 | 'http://www.w3.org/2000/svg',
44 | 'text',
45 | )
46 | assign(textElement, propsCopy, true, true)
47 | textElement.textContent = ALPHABET
48 | textElement.style.visibility = 'hidden'
49 | parentRef.appendChild(textElement)
50 | const bbox = textElement.getBBox()
51 | parentRef.removeChild(textElement)
52 | const averageCharWidth = bbox.width / ALPHABET.length
53 | const size = { x: averageCharWidth, y: bbox.height }
54 |
55 | sizeCache.set(cacheKey, size)
56 | return size
57 | }
58 |
59 | export { getAverageCharSize }
60 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/dom/createSize.ts:
--------------------------------------------------------------------------------
1 | import { type MaybeAccessor, access } from '@corvu/utils/reactivity'
2 | import {
3 | type Accessor,
4 | createEffect,
5 | createSignal,
6 | onCleanup,
7 | untrack,
8 | } from 'solid-js'
9 |
10 | const createSize = (props: {
11 | element: MaybeAccessor
12 | }): Accessor<[number, number] | null> => {
13 | const [size, setSize] = createSignal<[number, number] | null>(null)
14 |
15 | createEffect(() => {
16 | const element = access(props.element)
17 | if (!element) return
18 |
19 | syncSize(element)
20 |
21 | const observer = new ResizeObserver(resizeObserverCallback)
22 | observer.observe(element)
23 | onCleanup(() => {
24 | observer.disconnect()
25 | })
26 | })
27 |
28 | const resizeObserverCallback = ([entry]: ResizeObserverEntry[]) => {
29 | syncSize(entry!.target)
30 | }
31 |
32 | const syncSize = (element: Element) => {
33 | untrack(() => {
34 | const newSize = [element.clientWidth, element.clientHeight]
35 | const currentSize = size()
36 | const sizeChanged =
37 | currentSize === null ||
38 | currentSize[0] !== newSize[0] ||
39 | currentSize[1] !== newSize[1]
40 | if (!sizeChanged) return
41 | setSize([element.clientWidth, element.clientHeight])
42 | })
43 | }
44 |
45 | return size
46 | }
47 |
48 | export default createSize
49 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/dom/createSvgSize.ts:
--------------------------------------------------------------------------------
1 | import { type MaybeAccessor, access } from '@corvu/utils/reactivity'
2 | import { type Accessor, createEffect, createSignal, onCleanup } from 'solid-js'
3 | import { untrack } from 'solid-js/web'
4 |
5 | const createSvgSize = (props: {
6 | element: MaybeAccessor
7 | dimension: MaybeAccessor<'width' | 'height'>
8 | onSizeChange?: (size: number) => void
9 | onCleanup?: () => void
10 | }): Accessor => {
11 | const [size, setSize] = createSignal(null)
12 |
13 | createEffect(() => {
14 | const element = access(props.element)
15 | if (!element) return
16 |
17 | syncSize(element)
18 |
19 | const observer = new ResizeObserver(resizeObserverCallback)
20 | observer.observe(element)
21 | onCleanup(() => {
22 | observer.disconnect()
23 | props.onCleanup?.()
24 | })
25 | })
26 |
27 | const resizeObserverCallback = ([entry]: ResizeObserverEntry[]) => {
28 | syncSize(entry!.target)
29 | }
30 |
31 | const syncSize = (element: Element) => {
32 | const dimension = access(props.dimension)
33 | untrack(() => {
34 | const boundingClientRect = element.getBoundingClientRect()
35 | const newSize =
36 | dimension === 'width'
37 | ? boundingClientRect.width
38 | : boundingClientRect.height
39 | if (newSize === 0) return
40 | const sizeChanged = size() !== newSize
41 | if (!sizeChanged) return
42 | setSize(newSize)
43 | props.onSizeChange?.(newSize)
44 | })
45 | }
46 |
47 | return size
48 | }
49 |
50 | export default createSvgSize
51 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export type OverrideProps = Omit & P
2 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ChartContextType } from '@src/components/context'
2 | import { scaleBand } from 'd3-scale'
3 |
4 | const accessData = (data: unknown, dataKey: string | undefined): T[] => {
5 | return (
6 | dataKey
7 | ? (data as Record[]).map((entry) => entry[dataKey])
8 | : data
9 | ) as T[]
10 | }
11 |
12 | const gapToPadding = (gap: number | `${number}%`, bandwidth: number) => {
13 | if (typeof gap === 'number') return gap / bandwidth
14 | return Number.parseInt(gap.slice(0, -1)) / 100
15 | }
16 |
17 | const getBarPadding = (chartContext: ChartContextType) => {
18 | if (chartContext.bars().size === 0) return 0
19 |
20 | const left = chartContext.getInset('left')
21 | const right = chartContext.width() - chartContext.getInset('right')
22 |
23 | const chartWidth = right - left
24 | const dataLength = chartContext.data().length
25 |
26 | const barConfig = chartContext.barConfig()
27 | const bandGap = gapToPadding(barConfig.bandGap, chartWidth / dataLength)
28 |
29 | const bandScale = scaleBand()
30 | .domain(Array(dataLength).keys().map(String))
31 | .range([left, right])
32 | .paddingInner(bandGap)
33 |
34 | return bandScale.bandwidth() / 2
35 | }
36 |
37 | const pointDefined = (point: [number, number]) =>
38 | typeof point[0] === 'number' && typeof point[1] === 'number'
39 |
40 | export { accessData, gapToPadding, getBarPadding, pointDefined }
41 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/series/Area.tsx:
--------------------------------------------------------------------------------
1 | import { useChartContext } from '@src/components/context'
2 | import createBaseLine from '@src/lib/createBaseLine'
3 | import createPoints from '@src/lib/createPoints'
4 | import createSeries from '@src/lib/createSeries'
5 | import type { OverrideProps } from '@src/lib/types'
6 | import { accessData } from '@src/lib/utils'
7 | import Curve from '@src/shapes/Curve'
8 | import type { CurveFactory } from 'd3-shape'
9 | import {
10 | type ComponentProps,
11 | createMemo,
12 | createUniqueId,
13 | mergeProps,
14 | splitProps,
15 | } from 'solid-js'
16 |
17 | export type AreaProps = OverrideProps<
18 | Omit, 'd'>,
19 | {
20 | /**
21 | * The key in the data object used to render the area series. Don't provide a key if the data is a simple array of numbers.
22 | */
23 | dataKey?: string
24 | /**
25 | * The id of the y axis to use for the area series.
26 | * @defaultValue '0'
27 | */
28 | axisId?: string
29 | /**
30 | * The id of the stack to use for the area series.
31 | */
32 | stackId?: string
33 | /**
34 | * The curve function used to render the area series path.
35 | */
36 | curve?: CurveFactory
37 | /**
38 | * Whether to connect null values in the area series path.
39 | */
40 | connectNulls?: boolean
41 | }
42 | >
43 |
44 | /** Area series component.
45 | *
46 | * @data `data-sc-area` - Present on every area path element.
47 | */
48 | const Area = (props: AreaProps) => {
49 | const seriesId = createUniqueId()
50 |
51 | const defaultedProps = mergeProps(
52 | {
53 | axisId: '0',
54 | fill: 'currentColor',
55 | stroke: 'none',
56 | },
57 | props,
58 | )
59 |
60 | const [localProps, otherProps] = splitProps(defaultedProps, [
61 | 'dataKey',
62 | 'axisId',
63 | 'stackId',
64 | ])
65 |
66 | const chartContext = useChartContext()
67 |
68 | const data = createMemo(() =>
69 | accessData(chartContext.data(), localProps.dataKey),
70 | )
71 |
72 | createSeries({
73 | seriesId,
74 | dataKey: () => localProps.dataKey,
75 | axisId: () => localProps.axisId,
76 | stackId: () => localProps.stackId,
77 | data,
78 | chartContext,
79 | })
80 |
81 | const points = createPoints({
82 | dataKey: () => localProps.dataKey,
83 | axisId: () => localProps.axisId,
84 | stackId: () => localProps.stackId,
85 | data,
86 | chartContext,
87 | })
88 |
89 | const baseLine = createBaseLine({
90 | dataKey: () => localProps.dataKey,
91 | axisId: () => localProps.axisId,
92 | stackId: () => localProps.stackId,
93 | data,
94 | chartContext,
95 | })
96 |
97 | return (
98 |
104 | )
105 | }
106 |
107 | export default Area
108 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/series/Bar.tsx:
--------------------------------------------------------------------------------
1 | import { useChartContext } from '@src/components/context'
2 | import createBands from '@src/lib/createBands'
3 | import createBaseLine from '@src/lib/createBaseLine'
4 | import createPoints from '@src/lib/createPoints'
5 | import createSeries from '@src/lib/createSeries'
6 | import type { OverrideProps } from '@src/lib/types'
7 | import { accessData } from '@src/lib/utils'
8 | import {
9 | type ComponentProps,
10 | For,
11 | createEffect,
12 | createMemo,
13 | createUniqueId,
14 | mergeProps,
15 | onCleanup,
16 | splitProps,
17 | } from 'solid-js'
18 |
19 | export type BarProps = OverrideProps<
20 | Omit, 'x' | 'width' | 'y' | 'height'>,
21 | {
22 | /**
23 | * The key in the data object used to render the bar series. Don't provide a key if the data is a simple array of numbers.
24 | */
25 | dataKey?: string
26 | /**
27 | * The id of the y axis to use for the bar series.
28 | * @defaultValue '0'
29 | */
30 | axisId?: string
31 | /**
32 | * The id of the stack to use for the bar series.
33 | */
34 | stackId?: string
35 | }
36 | >
37 |
38 | /** Bar series component.
39 | *
40 | * @data `data-sc-bar-group` - Present on every bar group element.
41 | * @data `data-sc-bar` - Present on every bar rect element.
42 | */
43 | const Bar = (props: BarProps) => {
44 | const seriesId = createUniqueId()
45 |
46 | const defaultedProps = mergeProps(
47 | {
48 | axisId: '0',
49 | fill: 'currentColor',
50 | stroke: 'none',
51 | },
52 | props,
53 | )
54 |
55 | const [localProps, otherProps] = splitProps(defaultedProps, [
56 | 'dataKey',
57 | 'axisId',
58 | 'stackId',
59 | ])
60 |
61 | const chartContext = useChartContext()
62 |
63 | createEffect(() => {
64 | chartContext.registerBar(localProps.stackId ?? seriesId)
65 | onCleanup(() => chartContext.unregisterBar(localProps.stackId ?? seriesId))
66 | })
67 |
68 | const data = createMemo(() =>
69 | accessData(chartContext.data(), localProps.dataKey),
70 | )
71 |
72 | createSeries({
73 | seriesId,
74 | dataKey: () => localProps.dataKey,
75 | axisId: () => localProps.axisId,
76 | stackId: () => localProps.stackId,
77 | data,
78 | chartContext,
79 | })
80 |
81 | const points = createPoints({
82 | dataKey: () => localProps.dataKey,
83 | axisId: () => localProps.axisId,
84 | stackId: () => localProps.stackId,
85 | data,
86 | chartContext,
87 | })
88 |
89 | const baseLine = createBaseLine({
90 | dataKey: () => localProps.dataKey,
91 | axisId: () => localProps.axisId,
92 | stackId: () => localProps.stackId,
93 | data,
94 | chartContext,
95 | })
96 |
97 | const bands = createBands({
98 | seriesId,
99 | stackId: () => localProps.stackId,
100 | data,
101 | chartContext,
102 | })
103 |
104 | const bars = () => {
105 | const _points = points()
106 | let _baseLine = baseLine()
107 | if (!Array.isArray(_baseLine)) {
108 | _baseLine = new Array(_points.length).fill(_baseLine)
109 | }
110 | const _bands = bands()
111 | return new Array(_points.length).fill(null).map((_, i) => {
112 | const y = _points[i]![1]
113 | const baseLine = _baseLine[i]!
114 | return {
115 | x: _bands[i]!.x,
116 | width: _bands[i]!.width,
117 | y: y > baseLine ? baseLine : y,
118 | height: y > baseLine ? y - baseLine : baseLine - y,
119 | }
120 | })
121 | }
122 |
123 | return (
124 |
125 |
126 | {(bar) => }
127 |
128 |
129 | )
130 | }
131 |
132 | export default Bar
133 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/series/Line.tsx:
--------------------------------------------------------------------------------
1 | import { useChartContext } from '@src/components/context'
2 | import createPoints from '@src/lib/createPoints'
3 | import createSeries from '@src/lib/createSeries'
4 | import type { OverrideProps } from '@src/lib/types'
5 | import { accessData } from '@src/lib/utils'
6 | import Curve from '@src/shapes/Curve'
7 | import type { CurveFactory } from 'd3-shape'
8 | import {
9 | type ComponentProps,
10 | createMemo,
11 | createUniqueId,
12 | mergeProps,
13 | splitProps,
14 | } from 'solid-js'
15 |
16 | export type LineProps = OverrideProps<
17 | Omit, 'd'>,
18 | {
19 | /**
20 | * The key in the data object used to render the line series. Don't provide a key if the data is a simple array of numbers.
21 | */
22 | dataKey?: string
23 | /**
24 | * The id of the y axis to use for the line series.
25 | * @defaultValue '0'
26 | */
27 | axisId?: string
28 | /**
29 | * The id of the stack to use for the line series.
30 | */
31 | stackId?: string
32 | /**
33 | * The curve function used to render the line series path.
34 | */
35 | curve?: CurveFactory
36 | /**
37 | * Whether to connect null values in the line series path.
38 | */
39 | connectNulls?: boolean
40 | }
41 | >
42 |
43 | /** Line series component.
44 | *
45 | * @data `data-sc-line` - Present on every line path element.
46 | */
47 | const Line = (props: LineProps) => {
48 | const seriesId = createUniqueId()
49 |
50 | const defaultedProps = mergeProps(
51 | {
52 | axisId: '0',
53 | stroke: 'currentColor',
54 | fill: 'none',
55 | },
56 | props,
57 | )
58 |
59 | const [localProps, otherProps] = splitProps(defaultedProps, [
60 | 'dataKey',
61 | 'axisId',
62 | 'stackId',
63 | ])
64 |
65 | const chartContext = useChartContext()
66 |
67 | const data = createMemo(() =>
68 | accessData(chartContext.data(), localProps.dataKey),
69 | )
70 |
71 | createSeries({
72 | seriesId,
73 | dataKey: () => localProps.dataKey,
74 | axisId: () => localProps.axisId,
75 | stackId: () => localProps.stackId,
76 | data,
77 | chartContext,
78 | })
79 |
80 | const points = createPoints({
81 | dataKey: () => localProps.dataKey,
82 | axisId: () => localProps.axisId,
83 | stackId: () => localProps.stackId,
84 | data,
85 | chartContext,
86 | })
87 |
88 | return
89 | }
90 |
91 | export default Line
92 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/series/Point.tsx:
--------------------------------------------------------------------------------
1 | import { dataIf } from '@corvu/utils'
2 | import { useChartContext } from '@src/components/context'
3 | import createClosestTick from '@src/lib/createClosestTick'
4 | import createPoints from '@src/lib/createPoints'
5 | import createSeries from '@src/lib/createSeries'
6 | import type { OverrideProps } from '@src/lib/types'
7 | import { accessData, pointDefined } from '@src/lib/utils'
8 | import {
9 | type ComponentProps,
10 | For,
11 | createMemo,
12 | createUniqueId,
13 | mergeProps,
14 | splitProps,
15 | } from 'solid-js'
16 |
17 | export type PointProps = OverrideProps<
18 | Omit, 'cx' | 'cy'>,
19 | {
20 | /**
21 | * The key in the data object used to render the point series. Don't provide a key if the data is a simple array of numbers.
22 | */
23 | dataKey?: string
24 | /**
25 | * The id of the y axis to use for the point series.
26 | * @defaultValue '0'
27 | */
28 | axisId?: string
29 | /**
30 | * The id of the stack to use for the point series.
31 | */
32 | stackId?: string
33 | /**
34 | * `` element props to apply when the point is active.
35 | */
36 | activeProps?: Omit, 'cx' | 'cy'>
37 | }
38 | >
39 |
40 | /** Point series component.
41 | *
42 | * @data `data-sc-point-group` - Present on every point group element.
43 | * @data `data-sc-point` - Present on every point circle element.
44 | */
45 | const Point = (props: PointProps) => {
46 | const seriesId = createUniqueId()
47 |
48 | const defaultedProps = mergeProps(
49 | {
50 | axisId: '0',
51 | r: 4,
52 | fill: 'currentColor',
53 | },
54 | props,
55 | )
56 |
57 | const [localProps, otherProps] = splitProps(defaultedProps, [
58 | 'dataKey',
59 | 'axisId',
60 | 'stackId',
61 | 'activeProps',
62 | ])
63 |
64 | const chartContext = useChartContext()
65 |
66 | const data = createMemo(() =>
67 | accessData(chartContext.data(), localProps.dataKey),
68 | )
69 |
70 | createSeries({
71 | seriesId,
72 | dataKey: () => localProps.dataKey,
73 | axisId: () => localProps.axisId,
74 | stackId: () => localProps.stackId,
75 | data,
76 | chartContext,
77 | })
78 |
79 | const points = createPoints({
80 | dataKey: () => localProps.dataKey,
81 | axisId: () => localProps.axisId,
82 | stackId: () => localProps.stackId,
83 | data,
84 | chartContext,
85 | })
86 |
87 | const closestTick = createClosestTick({
88 | axis: () => 'x',
89 | chartContext,
90 | })
91 |
92 | const isActive = (index: number) =>
93 | chartContext.pointerInChart() && closestTick()?.index === index
94 |
95 | const circleProps = (index: number) => {
96 | if (!isActive(index)) return otherProps
97 | return mergeProps(otherProps, localProps.activeProps)
98 | }
99 |
100 | return (
101 |
102 |
103 | {(point, index) => (
104 |
111 | )}
112 |
113 |
114 | )
115 | }
116 |
117 | export default Point
118 |
--------------------------------------------------------------------------------
/packages/solid-charts/src/shapes/Curve.tsx:
--------------------------------------------------------------------------------
1 | import type { OverrideProps } from '@src/lib/types'
2 | import { pointDefined } from '@src/lib/utils'
3 | import {
4 | type Area,
5 | type CurveFactory,
6 | type Line,
7 | area,
8 | curveLinear,
9 | line,
10 | } from 'd3-shape'
11 | import {
12 | type ComponentProps,
13 | createMemo,
14 | mergeProps,
15 | splitProps,
16 | } from 'solid-js'
17 |
18 | export type CurveProps = OverrideProps<
19 | Omit, 'd'>,
20 | {
21 | points: [number, number][]
22 | curve?: CurveFactory
23 | baseLine?: number | number[]
24 | connectNulls?: boolean
25 | }
26 | >
27 |
28 | const Curve = (props: CurveProps) => {
29 | const defaultedProps = mergeProps(
30 | {
31 | baseLine: null,
32 | curve: curveLinear,
33 | connectNulls: false,
34 | },
35 | props,
36 | )
37 | const [localProps, otherProps] = splitProps(defaultedProps, [
38 | 'points',
39 | 'curve',
40 | 'baseLine',
41 | 'connectNulls',
42 | ])
43 |
44 | const path = createMemo(() =>
45 | getPath(
46 | localProps.curve,
47 | localProps.points,
48 | localProps.baseLine,
49 | localProps.connectNulls,
50 | ),
51 | )
52 |
53 | return
54 | }
55 |
56 | const getPath = (
57 | curve: CurveFactory,
58 | points: [number, number][],
59 | baseLine: number | number[] | null,
60 | connectNulls: boolean,
61 | ) => {
62 | if (connectNulls) {
63 | return createPathSegment(curve, points.filter(pointDefined), baseLine)
64 | }
65 |
66 | const segments: [number, number][][] = []
67 | let currentSegment: [number, number][] = []
68 |
69 | for (const point of points) {
70 | if (!pointDefined(point)) {
71 | if (currentSegment.length > 0) {
72 | segments.push(currentSegment)
73 | currentSegment = []
74 | }
75 | } else {
76 | currentSegment.push(point)
77 | }
78 | }
79 |
80 | if (currentSegment.length > 0) segments.push(currentSegment)
81 |
82 | const paths = segments.map((segment) =>
83 | createPathSegment(curve, segment, baseLine),
84 | )
85 | return paths.join(' ')
86 | }
87 |
88 | const createPathSegment = (
89 | curve: CurveFactory,
90 | points: [number, number][],
91 | baseLine: number | number[] | null,
92 | ) => {
93 | let lineFunction: Line<[number, number]> | Area<[number, number]>
94 | if (baseLine === null) {
95 | lineFunction = line().curve(curve)
96 | } else if (Array.isArray(baseLine)) {
97 | lineFunction = area()
98 | .curve(curve)
99 | .y0((_, i) => (baseLine[i] !== undefined ? baseLine[i]! : 0))
100 | } else {
101 | lineFunction = area().curve(curve).y0(baseLine)
102 | }
103 |
104 | return lineFunction(points)
105 | }
106 |
107 | export default Curve
108 |
--------------------------------------------------------------------------------
/packages/solid-charts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "target": "ESNext",
5 | "newLine": "LF",
6 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
7 | "moduleResolution": "Bundler",
8 | "noEmit": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "strictNullChecks": true,
12 | "esModuleInterop": true,
13 | "isolatedModules": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noUncheckedIndexedAccess": true,
16 | "skipLibCheck": true,
17 | "jsx": "preserve",
18 | "jsxImportSource": "solid-js",
19 | "verbatimModuleSyntax": true,
20 | "baseUrl": ".",
21 | "paths": {
22 | "@src/*": ["./src/*"]
23 | }
24 | },
25 | "exclude": ["node_modules", "dist"]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/solid-charts/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { solidPlugin } from 'esbuild-plugin-solid'
2 | import { type Options, defineConfig } from 'tsup'
3 |
4 | function generateConfig(jsx: boolean): Options {
5 | return {
6 | target: 'esnext',
7 | platform: 'browser',
8 | format: 'esm',
9 | clean: true,
10 | dts: !jsx,
11 | entry: ['src/index.ts', 'src/curves.ts'],
12 | outDir: 'dist/',
13 | treeshake: { preset: 'smallest' },
14 | replaceNodeEnv: true,
15 | esbuildOptions(options) {
16 | if (jsx) {
17 | options.jsx = 'preserve'
18 | }
19 | options.chunkNames = '[name]/[hash]'
20 | options.drop = ['console', 'debugger']
21 | },
22 | outExtension() {
23 | if (jsx) {
24 | return { js: '.jsx' }
25 | }
26 | return {}
27 | },
28 | esbuildPlugins: !jsx ? [solidPlugin({ solid: { generate: 'dom' } })] : [],
29 | }
30 | }
31 |
32 | export default defineConfig([generateConfig(false), generateConfig(true)])
33 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - dev
4 | - docs
5 | onlyBuiltDependencies:
6 | - '@biomejs/biome'
7 | - esbuild
8 | - sharp
9 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bump-minor-pre-major": true,
3 | "bump-patch-for-minor-pre-major": true,
4 | "tag-separator": "@",
5 | "include-component-in-tag": true,
6 | "include-v-in-tag": false,
7 | "packages": {
8 | "packages/solid-charts": {
9 | "component": "solid-charts"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": ["^build", "^typedoc"],
6 | "outputs": ["dist/**"]
7 | },
8 | "clean": {
9 | "cache": false
10 | },
11 | "dev": {
12 | "dependsOn": ["^build"],
13 | "persistent": true
14 | },
15 | "@solid-charts/docs#dev": {
16 | "dependsOn": ["^build", "^typedoc"],
17 | "persistent": true
18 | },
19 | "lint": {},
20 | "lint:fix": {},
21 | "preview": {
22 | "dependsOn": ["build"],
23 | "persistent": true
24 | },
25 | "typedoc": {
26 | "outputs": ["api.json"]
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------