├── .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 | [![codecov](https://codecov.io/gh/xr0master/chartjs-react/branch/master/graph/badge.svg)](https://codecov.io/gh/xr0master/chartjs-react) 6 | [![npm version](https://img.shields.io/npm/v/chartjs-react.svg)](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 | --------------------------------------------------------------------------------