├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── eslint.config.mjs
├── jest.config.js
├── package-lock.json
├── package.json
├── setupTests.ts
├── src
├── ReactChart.spec.tsx
├── ReactChart.tsx
└── utils
│ ├── generate-id
│ ├── generateID.spec.ts
│ └── generateID.ts
│ └── noop.ts
├── tsconfig.build.json
└── tsconfig.json
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | push:
9 | branches: [ master ]
10 | pull_request:
11 | branches: [ master ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | # Steps represent a sequence of tasks that will be executed as part of the job
24 | steps:
25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
26 | - uses: actions/checkout@v3
27 |
28 | - name: Install modules
29 | run: npm ci
30 |
31 | - name: Run tests
32 | run: npm run test
33 |
34 | - name: Upload coverage to Codecov
35 | uses: codecov/codecov-action@v3
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Specifies files to intentionally ignore when using Git
2 | # http://git-scm.com/docs/gitignore
3 |
4 | node_modules/
5 | coverage/
6 | dist/
7 | es/
8 | .idea/
9 | *.swp
10 | .DS_Store
11 | Thumbs.db
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Specifies files to intentionally ignore when using Git
2 | # http://git-scm.com/docs/gitignore
3 |
4 | # git ignore
5 | .github/
6 | node_modules/
7 | coverage/
8 | .idea/
9 | *.swp
10 | .DS_Store
11 | Thumbs.db
12 |
13 | # dev files
14 | .eslintrc.js
15 | .gitignore
16 | .npmignore
17 | jest.config.js
18 | setupTests.ts
19 | tsconfig.json
20 | tsconfig.build.json
21 |
22 | # source code files
23 | src/
24 | test/
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2020 xr0master
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chart.js React
2 |
3 | Tiny, written in TS, based on React hooks wrapper for Chart.js v4
4 |
5 | [](https://codecov.io/gh/xr0master/chartjs-react)
6 | [](https://www.npmjs.com/package/chartjs-react)
7 |
8 | ## Why?
9 |
10 | The main problem that the most popular package `react-chartjs-2` was written
11 | many years ago has a bunch of legacy code and issues
12 | (in 90% of cases it does not work without the redraw = true flag).
13 |
14 | The main idea was to completely rewrite code into modern React with hooks.
15 |
16 | The second goal, add custom React tooltips for Chart.js
17 |
18 | ## Version
19 |
20 | The version format is X.Y.Z, where
21 |
22 | - X - is chart.js major version
23 | - Y - is chartjs-react major version
24 | - Z - is chartjs-react minor version
25 |
26 | ### Current latest versions
27 | - 3.8.0 supports Chart.js version 3.9.1 and above
28 | - 4.0.0 supports Chart.js version 4.0.1 and above
29 |
30 | ## Support the project
31 |
32 | If you like to use this module please click the star button - it is very motivating.
33 |
34 | ## Quick Start
35 |
36 | Install chartjs-react using [npm](https://www.npmjs.com/):
37 |
38 | ```bash
39 | $ npm install chartjs-react
40 | ```
41 |
42 | ## Documentation
43 |
44 | TODO tooltips
45 |
46 | ## Examples
47 |
48 | **Bar chart on Chart.js v3 (date-fns)**
49 |
50 | ```tsx
51 | import {
52 | BarController,
53 | LinearScale,
54 | BarElement,
55 | TimeScale,
56 | Tooltip,
57 | } from 'chart.js';
58 | import 'chartjs-adapter-date-fns';
59 | import { enUS } from 'date-fns/locale';
60 | import { ReactChart } from 'chartjs-react';
61 |
62 | // Register modules,
63 | // this example for time scale and linear scale
64 | ReactChart.register(BarController, LinearScale, BarElement, TimeScale, Tooltip);
65 |
66 | // options of chart similar to v2 with a few changes
67 | // https://www.chartjs.org/docs/next/getting-started/v3-migration/
68 | const chartOption = {
69 | scales: {
70 | x: {
71 | type: 'time',
72 | adapters: {
73 | date: enUS,
74 | },
75 | },
76 | y: {
77 | type: 'linear',
78 | },
79 | },
80 | };
81 |
82 | // data of chart similar to v2, check the migration guide
83 | const chartData = {};
84 |
85 | const BarChart = () => {
86 | return (
87 |
93 | );
94 | };
95 | ```
96 |
97 | **Get the chart instance**
98 |
99 | ```tsx
100 | import { Chart } from 'chart.js';
101 |
102 | onEvent = () => {
103 | const myChartInstance = Chart.getChart('unique-chart-id');
104 | // Do your stuff with the chart instance
105 | // Note: the chart should be mounted
106 | };
107 |
108 | const BarChart = () => {
109 | return (
110 |
117 | );
118 | };
119 | ```
120 |
121 | ## TODO
122 |
123 | - Added chart tooltip as children (after release v3)
124 |
125 | ## License
126 |
127 | [MIT](./LICENSE)
128 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import eslint from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 | import stylistic from '@stylistic/eslint-plugin';
4 | import reactHooks from 'eslint-plugin-react-hooks';
5 | import reactRefresh from 'eslint-plugin-react-refresh';
6 |
7 | export default tseslint.config(
8 | eslint.configs.recommended,
9 | ...tseslint.configs.recommendedTypeChecked,
10 | {
11 | languageOptions: {
12 | parserOptions: {
13 | projectService: true,
14 | tsconfigRootDir: import.meta.dirname,
15 | },
16 | },
17 | },
18 | {
19 | files: ['**/*.ts', '**/*.tsx'],
20 | plugins: {
21 | '@stylistic': stylistic,
22 | 'react-hooks': reactHooks,
23 | 'react-refresh': reactRefresh,
24 | },
25 | rules: {
26 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
27 | '@typescript-eslint/explicit-function-return-type': 'off',
28 | '@typescript-eslint/explicit-module-boundary-types': 'off',
29 | },
30 | },
31 | );
32 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('jest').Config} */
2 | const jestConfig = {
3 | collectCoverage: true,
4 | coverageDirectory: 'coverage',
5 | transform: {
6 | '^.+\\.ts?$': [
7 | 'ts-jest',
8 | {
9 | diagnostics: {
10 | warnOnly: true,
11 | },
12 | },
13 | ],
14 | },
15 | testRegex: '((\\.|/)(spec))\\.(tsx?)$',
16 | moduleFileExtensions: ['js', 'ts', 'tsx'],
17 | modulePaths: ['src'],
18 | moduleNameMapper: {
19 | 'chart.js': '/node_modules/chart.js/dist/chart.umd.js',
20 | },
21 | testPathIgnorePatterns: ['/node_modules/'],
22 | setupFilesAfterEnv: ['/setupTests.ts'],
23 | testEnvironment: '@happy-dom/jest-environment',
24 | preset: 'ts-jest',
25 | };
26 |
27 | module.exports = jestConfig;
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartjs-react",
3 | "version": "4.3.0",
4 | "description": "TypeScript React wrapper for Chart.js with hooks and tooltip",
5 | "private": false,
6 | "author": "Sergey Khomushin ",
7 | "license": "MIT",
8 | "main": "es/ReactChart.js",
9 | "types": "es/ReactChart.d.ts",
10 | "keywords": [
11 | "chartjs react",
12 | "chartjs hooks",
13 | "chartjs react typescript",
14 | "chartjs tooltip"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/xr0master/chartjs-react.git"
19 | },
20 | "scripts": {
21 | "_build-ts": "tsc --project ./tsconfig.build.json",
22 | "_clean": "rm -rf ./es",
23 | "_eslint": "eslint 'src/**/*.{ts,tsx}'",
24 | "build": "npm run _clean && npm run _eslint && npm run _build-ts",
25 | "test": "jest --coverage",
26 | "lint": "tsc --noEmit && npm run _eslint"
27 | },
28 | "peerDependencies": {
29 | "chart.js": ">=3.9.0",
30 | "react": ">=16.8.0"
31 | },
32 | "devDependencies": {
33 | "@eslint/js": "^9.17.0",
34 | "@happy-dom/jest-environment": "^15.11.7",
35 | "@stylistic/eslint-plugin": "^2.12.1",
36 | "@testing-library/jest-dom": "^6.6.3",
37 | "@testing-library/react": "^16.1.0",
38 | "@types/node": "^22.10.2",
39 | "@types/react": "^19.0.2",
40 | "chart.js": "^4.4.7",
41 | "eslint": "^9.17.0",
42 | "eslint-plugin-react-hooks": "^5.1.0",
43 | "eslint-plugin-react-refresh": "^0.4.16",
44 | "jest": "^29.7.0",
45 | "prettier": "^3.4.2",
46 | "react": "^19.0.0",
47 | "ts-jest": "^29.2.5",
48 | "typescript": "^5.7.2",
49 | "typescript-eslint": "^8.18.2"
50 | },
51 | "prettier": {
52 | "trailingComma": "all",
53 | "singleQuote": true
54 | },
55 | "bugs": {
56 | "url": "https://github.com/xr0master/chartjs-react/issues"
57 | },
58 | "homepage": "https://github.com/xr0master/chartjs-react#readme"
59 | }
60 |
--------------------------------------------------------------------------------
/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { expect } from '@jest/globals';
2 | import '@testing-library/jest-dom';
3 | import { TestingLibraryMatchers } from '@testing-library/jest-dom/types/matchers-standalone';
4 |
5 | declare module '@jest/expect' {
6 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
7 | export interface Matchers
8 | extends TestingLibraryMatchers {}
9 | }
10 |
--------------------------------------------------------------------------------
/src/ReactChart.spec.tsx:
--------------------------------------------------------------------------------
1 | import { it, expect, jest } from '@jest/globals';
2 | import { render, screen } from '@testing-library/react';
3 | import React from 'react';
4 | import type { Chart } from 'chart.js';
5 | import { ReactChart } from './ReactChart';
6 |
7 | jest.mock('chart.js', () => ({
8 | ...jest.requireActual('chart.js'),
9 | Chart: jest.fn(() => ({
10 | update: jest.fn(),
11 | destroy: jest.fn(),
12 | })),
13 | }));
14 |
15 | it('should be in document', function () {
16 | render();
17 | expect(screen.getByRole('chart')).toBeInTheDocument();
18 | });
19 |
--------------------------------------------------------------------------------
/src/ReactChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useCallback, useState } from 'react';
2 | import { Chart } from 'chart.js';
3 |
4 | import type {
5 | ChartOptions,
6 | ChartData,
7 | ChartType,
8 | Plugin,
9 | UpdateMode,
10 | } from 'chart.js';
11 |
12 | import { generateID } from './utils/generate-id/generateID';
13 | import { noop } from './utils/noop';
14 |
15 | export interface ChartProps {
16 | data: ChartData;
17 | options: ChartOptions;
18 | type: ChartType;
19 | plugins?: Plugin[];
20 | updateMode?: UpdateMode;
21 | id?: string;
22 | height?: number;
23 | width?: number;
24 | }
25 |
26 | export const ReactChart = ({
27 | id,
28 | data,
29 | options,
30 | type,
31 | plugins,
32 | updateMode,
33 | height,
34 | width,
35 | }: ChartProps) => {
36 | const chartInstance = useRef({
37 | update: noop,
38 | destroy: noop,
39 | } as Chart);
40 | const [CHART_ID] = useState(id || generateID('Chart'));
41 |
42 | useEffect(() => {
43 | chartInstance.current.data = data;
44 | chartInstance.current.options = options;
45 |
46 | chartInstance.current.update(updateMode);
47 | }, [data, options]);
48 |
49 | const nodeRef = useCallback<(node: HTMLCanvasElement | null) => void>(
50 | (node) => {
51 | chartInstance.current.destroy();
52 |
53 | if (node) {
54 | chartInstance.current = new Chart(node, {
55 | type,
56 | data,
57 | options,
58 | plugins,
59 | });
60 | }
61 | },
62 | [],
63 | );
64 |
65 | return (
66 |
73 | );
74 | };
75 |
76 | // The `register` is a static method and can be safely bounded
77 | // eslint-disable-next-line @typescript-eslint/unbound-method
78 | ReactChart.register = Chart.register || noop;
79 |
--------------------------------------------------------------------------------
/src/utils/generate-id/generateID.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect } from '@jest/globals';
2 | import { generateID } from './generateID';
3 |
4 | it('should be 8 symbols', () => {
5 | expect(generateID()).toHaveLength(8);
6 | });
7 |
8 | it('should generate unique id', () => {
9 | expect(generateID()).not.toEqual(generateID());
10 | });
11 |
12 | it('should contain the prefix as "test"', () => {
13 | const prefix = 'test';
14 | expect(generateID(prefix)).toContain(prefix);
15 | });
16 |
--------------------------------------------------------------------------------
/src/utils/generate-id/generateID.ts:
--------------------------------------------------------------------------------
1 | export const generateID = (prefix = '') => {
2 | const hash = Math.random().toString(36).slice(-7);
3 | return `${prefix}-${hash}`;
4 | };
5 |
--------------------------------------------------------------------------------
/src/utils/noop.ts:
--------------------------------------------------------------------------------
1 | export const noop = () => {};
2 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"],
4 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
7 | "allowJs": false,
8 | "skipLibCheck": true,
9 | "declaration": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "removeComments": true,
15 | "sourceMap": false,
16 | "jsx": "react-jsx",
17 | "baseUrl": ".",
18 | "outDir": "es"
19 | },
20 | "include": ["src", "setupTests.ts"]
21 | }
22 |
--------------------------------------------------------------------------------