├── .eslintignore ├── docs ├── assets │ ├── align.png │ ├── anchor.png │ └── clamp.png ├── .vuepress │ └── public │ │ ├── favicon.ico │ │ ├── favicon.png │ │ └── _redirects ├── samples │ ├── register.js │ ├── defaults.js │ ├── advanced │ │ ├── custom-labels.md │ │ └── multiple-labels.md │ ├── utils.js │ ├── events │ │ ├── listeners.md │ │ ├── highlight.md │ │ └── selection.md │ ├── charts │ │ ├── polar.md │ │ ├── radar.md │ │ ├── line.md │ │ ├── bar.md │ │ ├── bubble.md │ │ └── doughnut.md │ └── scriptable │ │ ├── mirror.md │ │ ├── indices.md │ │ ├── dataset.md │ │ ├── data.md │ │ └── interactions.md ├── README.md └── guide │ ├── README.md │ ├── typescript.md │ ├── migration.md │ ├── formatting.md │ ├── events.md │ ├── positioning.md │ └── labels.md ├── src ├── .eslintrc.yml ├── defaults.js ├── utils.js └── hitbox.js ├── test ├── fixtures │ ├── options.clip │ │ ├── clip-values.png │ │ ├── clip-indexable.png │ │ ├── clip-scriptable.png │ │ ├── clip-values.js │ │ ├── clip-scriptable.js │ │ └── clip-indexable.js │ ├── drawing │ │ ├── draw-on-top-bar-side.png │ │ ├── draw-on-top-bar-stacked.png │ │ ├── draw-skipped-values-in-correct-order.png │ │ ├── draw-on-top-bar-side.js │ │ ├── draw-on-top-bar-stacked.js │ │ └── draw-skipped-values-in-correct-order.js │ ├── options.align │ │ ├── align-indexable.png │ │ ├── align-presets-arc.png │ │ ├── align-scriptable.png │ │ ├── align-angles-linear.png │ │ ├── align-angles-radial.png │ │ ├── align-presets-bar-vertical.png │ │ ├── align-presets-point-radial.png │ │ ├── align-presets-bar-horizontal.png │ │ ├── align-presets-point-horizontal.png │ │ ├── align-presets-bar-vertical-zero.png │ │ ├── align-presets-bar-horizontal-zero.png │ │ ├── align-indexable.js │ │ ├── align-angles-radial.js │ │ ├── align-presets-arc.js │ │ ├── align-scriptable.js │ │ ├── align-presets-bar-vertical-zero.js │ │ ├── align-presets-bar-vertical.js │ │ ├── align-presets-bar-horizontal-zero.js │ │ ├── align-presets-bar-horizontal.js │ │ ├── align-angles-linear.js │ │ ├── align-presets-point-horizontal.js │ │ └── align-presets-point-radial.js │ ├── options.clamp │ │ ├── clamp-indexable.png │ │ ├── clamp-scriptable.png │ │ ├── clamp-values-bar-vertical.png │ │ ├── clamp-values-bar-horizontal.png │ │ ├── clamp-values-bar-vertical-negative.png │ │ ├── clamp-values-bar-vertical-positive.png │ │ ├── clamp-values-bar-horizontal-negative.png │ │ ├── clamp-values-bar-horizontal-positive.png │ │ ├── clamp-indexable.js │ │ ├── clamp-scriptable.js │ │ ├── clamp-values-bar-vertical-positive.js │ │ ├── clamp-values-bar-vertical-negative.js │ │ ├── clamp-values-bar-vertical.js │ │ ├── clamp-values-bar-horizontal-positive.js │ │ ├── clamp-values-bar-horizontal-negative.js │ │ └── clamp-values-bar-horizontal.js │ ├── options.labels │ │ ├── labels-default.png │ │ ├── labels-value.png │ │ ├── labels-indexable.png │ │ ├── labels-scriptable.png │ │ ├── labels-default.js │ │ ├── labels-indexable.js │ │ ├── labels-scriptable.js │ │ └── labels-value.js │ ├── options.anchor │ │ ├── anchor-indexable.png │ │ ├── anchor-presets-arc.png │ │ ├── anchor-scriptable.png │ │ ├── anchor-presets-bar-horizontal.png │ │ ├── anchor-presets-bar-vertical.png │ │ ├── anchor-presets-point-radial.png │ │ ├── anchor-presets-point-horizontal.png │ │ ├── anchor-presets-fallback-width-height.png │ │ ├── anchor-presets-fallback-no-width-height.png │ │ ├── anchor-indexable.js │ │ ├── anchor-presets-arc.js │ │ ├── anchor-scriptable.js │ │ ├── anchor-presets-bar-vertical.js │ │ ├── anchor-presets-point-horizontal.js │ │ ├── anchor-presets-bar-horizontal.js │ │ ├── anchor-presets-point-radial.js │ │ ├── anchor-presets-fallback-no-width-height.js │ │ └── anchor-presets-fallback-width-height.js │ ├── options.opacity │ │ ├── opacity-values.png │ │ ├── opacity-indexable.png │ │ ├── opacity-scriptable.png │ │ ├── opacity-values.js │ │ ├── opacity-indexable.js │ │ └── opacity-scriptable.js │ └── options.display │ │ ├── display-indexable.png │ │ ├── display-scriptable.png │ │ ├── display-values-auto.png │ │ ├── display-values-bool.png │ │ ├── display-scriptable.js │ │ ├── display-values-bool.js │ │ ├── display-values-auto.js │ │ └── display-indexable.js ├── .eslintrc.yml ├── plugins.js ├── specs │ ├── module.spec.js │ ├── options.spec.js │ ├── drawing.spec.js │ ├── utils.spec.js │ └── defaults.spec.js └── index.js ├── .gitignore ├── .eslintrc.yml ├── types ├── test │ ├── tsconfig.json │ ├── options.undefined.ts │ ├── options.default.ts │ ├── exports.ts │ ├── options.context.ts │ ├── options.inline.ts │ ├── options.indexable.ts │ ├── options.scriptable.ts │ └── options.basic.ts ├── index.d.ts ├── context.d.ts └── .eslintrc.js ├── scripts ├── create-bower-json.js ├── publish-to-npm.js ├── attach-gh-assets.js ├── utils.js ├── create-release-tag.js └── create-packages.js ├── .codeclimate.yml ├── .github └── workflows │ ├── publish.yml │ ├── release.yml │ └── ci.yml ├── LICENSE.md ├── rollup.config.js ├── package.json ├── README.md └── karma.conf.js /.eslintignore: -------------------------------------------------------------------------------- 1 | !docs/.vuepress 2 | **/*{.,-}min.js 3 | dist/**/* 4 | -------------------------------------------------------------------------------- /docs/assets/align.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/docs/assets/align.png -------------------------------------------------------------------------------- /docs/assets/anchor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/docs/assets/anchor.png -------------------------------------------------------------------------------- /docs/assets/clamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/docs/assets/clamp.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/docs/.vuepress/public/favicon.png -------------------------------------------------------------------------------- /src/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | plugins: [es] 2 | 3 | extends: [ 4 | plugin:es/restrict-to-es2015 5 | ] 6 | 7 | rules: 8 | es/no-modules: 0 9 | -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clip/clip-values.png -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-on-top-bar-side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/drawing/draw-on-top-bar-side.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clip/clip-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clip/clip-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.labels/labels-default.png -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.labels/labels-value.png -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-on-top-bar-stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/drawing/draw-on-top-bar-stacked.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-arc.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.labels/labels-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.opacity/opacity-values.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintcache 3 | .idea 4 | .vscode/ 5 | bower.json 6 | cc-test-reporter 7 | coverage/ 8 | dist/ 9 | node_modules/ 10 | *.stackdump 11 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-angles-linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-angles-linear.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-angles-radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-angles-radial.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-arc.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.display/display-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.display/display-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.display/display-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.display/display-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.labels/labels-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-indexable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.opacity/opacity-indexable.png -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-scriptable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.opacity/opacity-scriptable.png -------------------------------------------------------------------------------- /test/fixtures/options.display/display-values-auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.display/display-values-auto.png -------------------------------------------------------------------------------- /test/fixtures/options.display/display-values-bool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.display/display-values-bool.png -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: chartjs 2 | 3 | parserOptions: 4 | ecmaVersion: 8 5 | sourceType: module 6 | 7 | env: 8 | browser: true 9 | node: true 10 | es6: true 11 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-vertical.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-bar-vertical.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-point-radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-point-radial.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-horizontal.png -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-skipped-values-in-correct-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/drawing/draw-skipped-values-in-correct-order.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-bar-horizontal.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-point-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-point-horizontal.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-bar-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-bar-horizontal.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-bar-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-bar-vertical.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-point-radial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-point-radial.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-vertical-zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-bar-vertical-zero.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-point-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-point-horizontal.png -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-horizontal-zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.align/align-presets-bar-horizontal-zero.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-vertical-negative.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical-positive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-vertical-positive.png -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-fallback-width-height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-fallback-width-height.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-horizontal-negative.png -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal-positive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.clamp/clamp-values-bar-horizontal-positive.png -------------------------------------------------------------------------------- /docs/samples/register.js: -------------------------------------------------------------------------------- 1 | import {Chart, registerables} from 'chart.js'; 2 | import plugin from '../../dist/chartjs-plugin-datalabels.js'; 3 | 4 | Chart.register(...registerables); 5 | Chart.register(plugin); 6 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-fallback-no-width-height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chartjs/chartjs-plugin-datalabels/HEAD/test/fixtures/options.anchor/anchor-presets-fallback-no-width-height.png -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | jasmine: true 3 | 4 | globals: 5 | __karma__: true 6 | 7 | # https://eslint.org/docs/rules/ 8 | rules: 9 | # Best Practices 10 | complexity: 0 11 | no-new-func: 0 12 | -------------------------------------------------------------------------------- /types/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noEmit": true, 8 | }, 9 | "include": [ 10 | "*.ts", 11 | "../*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/samples/defaults.js: -------------------------------------------------------------------------------- 1 | import {defaults} from 'chart.js'; 2 | 3 | defaults.set({ 4 | plugins: { 5 | legend: { 6 | display: false 7 | }, 8 | title: { 9 | display: false 10 | }, 11 | tooltip: { 12 | enabled: false 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /docs/.vuepress/public/_redirects: -------------------------------------------------------------------------------- 1 | # GitBook -> VuePress migration 2 | /installation /guide/getting-started.html 3 | /options /guide/options.html 4 | /positioning /guide/positioning.html 5 | /formatting /guide/formatting.html 6 | /events /guide/events.html 7 | -------------------------------------------------------------------------------- /types/test/options.undefined.ts: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | 3 | const chart = new Chart('id', { 4 | type: 'bar', 5 | data: { 6 | labels: [], 7 | datasets: [ 8 | { 9 | data: [], 10 | // no datalabels options 11 | } 12 | ] 13 | }, 14 | options: { 15 | plugins: { 16 | // no datalabels options 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/create-bower-json.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pkg = require('../package.json'); 4 | 5 | const output = path.resolve(__dirname, '../bower.json'); 6 | const json = JSON.stringify({ 7 | name: pkg.name, 8 | description: pkg.description, 9 | homepage: pkg.homepage, 10 | license: pkg.license, 11 | version: pkg.version, 12 | main: pkg.main, 13 | }, null, 2); 14 | 15 | fs.writeFileSync(output, json + '\n'); 16 | -------------------------------------------------------------------------------- /types/test/options.default.ts: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import {Options} from '../options'; 3 | 4 | const options: Options = { 5 | // all optional datalabels options 6 | }; 7 | 8 | const chart = new Chart('id', { 9 | type: 'bar', 10 | data: { 11 | labels: [], 12 | datasets: [ 13 | { 14 | data: [], 15 | datalabels: options 16 | } 17 | ] 18 | }, 19 | options: { 20 | plugins: { 21 | datalabels: options 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'jasmine-chart-area': { 3 | afterDraw: function(chart) { 4 | var ctx = chart.ctx; 5 | var area = chart.chartArea; 6 | 7 | ctx.save(); 8 | ctx.beginPath(); 9 | ctx.rect( 10 | area.left, 11 | area.top, 12 | area.right - area.left, 13 | area.bottom - area.top); 14 | ctx.strokeStyle = 'blue'; 15 | ctx.lineWidth = 2; 16 | ctx.stroke(); 17 | ctx.restore(); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /hero.svg 4 | actionText: Get Started → 5 | actionLink: /guide/ 6 | footer: MIT Licensed | Copyright © 2017-2021 chartjs-plugin-datalabels contributors 7 | features: 8 | - title: Flexible 9 | details: Compatible with all types of charts (bar, line, doughnut, radar, etc.) 10 | - title: Customizable 11 | details: Appearance and position of each label are fully and dynamically controllable. 12 | - title: Interactive 13 | details: Labels can react to user interactions or element events using scriptable options. 14 | --- 15 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | argument-count: 4 | enabled: true 5 | config: 6 | threshold: 5 7 | complex-logic: 8 | enabled: false 9 | method-complexity: 10 | enabled: false 11 | method-lines: 12 | enabled: false 13 | similar-code: 14 | enabled: false 15 | plugins: 16 | duplication: 17 | enabled: true 18 | config: 19 | languages: 20 | - javascript 21 | fixme: 22 | enabled: true 23 | exclude_patterns: 24 | - "coverage/" 25 | - "docs/" 26 | - "scripts/" 27 | - "test/" 28 | - "*.js" 29 | - "*.json" 30 | - "*.md" 31 | - ".*" 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/actions/reference/workflow-syntax-for-github-actions 2 | 3 | name: Publish 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | publish-to-npm: 11 | if: github.repository_owner == 'chartjs' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | registry-url: https://registry.npmjs.org/ 18 | - run: npm ci 19 | - run: node scripts/publish-to-npm.js 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 22 | -------------------------------------------------------------------------------- /types/test/exports.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/130 2 | import {Chart} from 'chart.js'; 3 | import {Context} from '../index'; 4 | import Plugin from '../index'; 5 | 6 | // Plugin instance 7 | Chart.register(Plugin); 8 | Chart.unregister(Plugin); 9 | 10 | const chart = new Chart('id', { 11 | data: { 12 | labels: [], 13 | datasets: [] 14 | }, 15 | options: {}, 16 | type: 'bar', 17 | plugins: [Plugin] 18 | }); 19 | 20 | // Scriptable context 21 | const ctx: Context = { 22 | active: true, 23 | chart: chart, 24 | datasetIndex: 0, 25 | dataIndex: 0, 26 | dataset: { 27 | data: [] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import {ChartType, Plugin} from 'chart.js'; 2 | import {Options} from './options'; 3 | 4 | declare module 'chart.js' { 5 | interface ChartDatasetProperties { 6 | /** 7 | * Per dataset datalabels plugin options. 8 | * @since 0.1.0 9 | */ 10 | datalabels?: Options; 11 | } 12 | 13 | interface PluginOptionsByType { 14 | /** 15 | * Per chart datalabels plugin options. 16 | * @since 0.1.0 17 | */ 18 | datalabels?: Options; 19 | } 20 | } 21 | 22 | declare const plugin: Plugin; 23 | 24 | export * from './context'; 25 | 26 | export default plugin; 27 | -------------------------------------------------------------------------------- /scripts/publish-to-npm.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | /* eslint-disable no-console */ 3 | 4 | const {run, semtag} = require('./utils'); 5 | const {version} = require('../package.json'); 6 | 7 | (async() => { 8 | const tags = (await run('npm dist-tag ls')).split('\n').reduce((acc, line) => { 9 | const matches = (/^([^:]+): (.+)$/).exec(line); 10 | acc[matches[1]] = matches[2]; 11 | return acc; 12 | }, {}); 13 | 14 | const tag = semtag(tags, version); 15 | 16 | console.info(`Publishing version ${version} (@${tag})`); 17 | 18 | await run(`npm publish --tag ${tag}`); 19 | 20 | })().catch((error) => { 21 | console.error(`Failed to publish to npm: ${error.message || error}.`); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/actions/reference/workflow-syntax-for-github-actions 2 | 3 | name: Release 4 | 5 | on: workflow_dispatch 6 | 7 | jobs: 8 | create-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | - run: npm ci 14 | - run: npm run build 15 | - run: npm run bower 16 | - run: npm run package 17 | - run: node scripts/attach-gh-assets.js 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | - run: node scripts/create-release-tag.js 21 | env: 22 | GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} 23 | GH_AUTH_NAME: ${{ secrets.GH_AUTH_NAME }} 24 | -------------------------------------------------------------------------------- /types/test/options.context.ts: -------------------------------------------------------------------------------- 1 | import {Chart, ChartEvent} from 'chart.js'; 2 | import {Context} from '../context'; 3 | import {Options} from '../options'; 4 | 5 | interface CustomContext extends Context { 6 | foo?: number; 7 | } 8 | 9 | const options: Options = { 10 | rotation: (ctx: CustomContext) => ctx.foo || 0, 11 | listeners: { 12 | click: (ctx: CustomContext, event: ChartEvent) => { 13 | ctx.foo = 42; 14 | } 15 | }, 16 | }; 17 | 18 | const chart = new Chart('id', { 19 | type: 'bar', 20 | data: { 21 | labels: [], 22 | datasets: [ 23 | { 24 | data: [], 25 | datalabels: options, 26 | } 27 | ] 28 | }, 29 | options: { 30 | plugins: { 31 | datalabels: options 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /test/specs/module.spec.js: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import plugin from 'chartjs-plugin-datalabels'; 3 | 4 | describe('module', function() { 5 | it ('should be globally exported in ChartDataLabels', function() { 6 | expect(typeof window.ChartDataLabels).toBe('object'); 7 | expect(window.ChartDataLabels).toBe(plugin); 8 | }); 9 | 10 | it ('should be referenced with id "datalabels"', function() { 11 | expect(plugin.id).toBe('datalabels'); 12 | }); 13 | 14 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/42 15 | it ('should not be globally registered', function() { 16 | expect(Chart.registry.plugins.get('datalabels')).toBeUndefined(); 17 | expect(() => Chart.registry.getPlugin('datalabels')).toThrow(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-indexable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'bar', 4 | data: { 5 | labels: [0, 1, 2, 3, 4], 6 | datasets: [{ 7 | data: [1, 1, 1, 1, 1], 8 | datalabels: { 9 | backgroundColor: '#00ff77' 10 | } 11 | }] 12 | }, 13 | options: { 14 | layout: { 15 | padding: 16 16 | }, 17 | plugins: { 18 | datalabels: { 19 | anchor: ['start', 'center', 'end', 'center', 'start'], 20 | borderColor: 'black', 21 | borderWidth: 2, 22 | font: { 23 | size: 0 24 | }, 25 | offset: 0, 26 | padding: 8 27 | } 28 | } 29 | } 30 | }, 31 | options: { 32 | canvas: { 33 | height: 256, 34 | width: 256 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/actions/reference/workflow-syntax-for-github-actions 2 | 3 | name: CI 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | build-and-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | - run: npm ci 14 | - run: npm run build 15 | - run: npm run lint 16 | - name: Run tests 17 | uses: GabrielBB/xvfb-action@v1 18 | with: 19 | run: npm run test 20 | - if: env.CC_TEST_REPORTER_ID 21 | uses: paambaati/codeclimate-action@v3.2.0 22 | env: 23 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 24 | - uses: actions/upload-artifact@v3 25 | with: 26 | path: dist/ 27 | name: chartjs-plugin-datalabels 28 | if-no-files-found: error 29 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-arc.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | 3 | ['end', 'center', 'start'].forEach(function(anchor) { 4 | datasets.push({ 5 | data: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], 6 | datalabels: { 7 | anchor: anchor 8 | } 9 | }); 10 | }); 11 | 12 | export default { 13 | config: { 14 | type: 'doughnut', 15 | data: { 16 | datasets: datasets 17 | }, 18 | options: { 19 | cutoutPercentage: 50, 20 | layout: { 21 | padding: 16 22 | }, 23 | plugins: { 24 | datalabels: { 25 | backgroundColor: '#00ff77', 26 | borderColor: 'black', 27 | borderWidth: 2, 28 | font: { 29 | size: 0 30 | }, 31 | padding: 8 32 | } 33 | } 34 | } 35 | }, 36 | options: { 37 | canvas: { 38 | height: 512, 39 | width: 512 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-scriptable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'bar', 4 | data: { 5 | labels: [0, 1, 2, 3, 4], 6 | datasets: [{ 7 | data: [-1, 1, 2, 1, -2], 8 | datalabels: { 9 | backgroundColor: '#00ff77' 10 | } 11 | }] 12 | }, 13 | options: { 14 | plugins: { 15 | datalabels: { 16 | anchor: function(context) { 17 | var data = context.dataset.data[context.dataIndex]; 18 | return data > 1 ? 'start' : data < -1 ? 'end' : 'center'; 19 | }, 20 | borderColor: 'black', 21 | borderWidth: 2, 22 | font: { 23 | size: 0 24 | }, 25 | offset: 0, 26 | padding: 8 27 | } 28 | } 29 | } 30 | }, 31 | options: { 32 | canvas: { 33 | height: 256, 34 | width: 256 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-bar-vertical.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | 3 | ['start', 'center', 'end'].forEach(function(anchor) { 4 | datasets.push({ 5 | data: [0, 4, -4], 6 | datalabels: { 7 | anchor: anchor 8 | } 9 | }); 10 | }); 11 | 12 | export default { 13 | config: { 14 | type: 'bar', 15 | data: { 16 | labels: [0, 1, 2], 17 | datasets: datasets 18 | }, 19 | options: { 20 | layout: { 21 | padding: { 22 | bottom: 20, 23 | top: 20 24 | } 25 | }, 26 | plugins: { 27 | datalabels: { 28 | backgroundColor: '#00ff77', 29 | borderColor: 'black', 30 | borderWidth: 2, 31 | font: { 32 | size: 0 33 | }, 34 | padding: 8 35 | } 36 | } 37 | } 38 | }, 39 | options: { 40 | canvas: { 41 | height: 256, 42 | width: 512 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /types/context.d.ts: -------------------------------------------------------------------------------- 1 | import {Chart, ChartDataset} from 'chart.js'; 2 | 3 | /** 4 | * The context is used to give contextual information when resolving options. 5 | * It mainly applies to scriptable options, event listeners and `formatter`. 6 | */ 7 | export interface Context { 8 | /** 9 | * Whether the associated element is hovered by the user. 10 | * @see https://www.chartjs.org/docs/latest/configuration/interactions.html 11 | * @since 0.3.0 12 | */ 13 | active: boolean; 14 | 15 | /** 16 | * The associated chart. 17 | * @since 0.1.0 18 | */ 19 | chart: Chart; 20 | 21 | /** 22 | * Index of the current data. 23 | * @since 0.1.0 24 | */ 25 | dataIndex: number; 26 | 27 | /** 28 | * The current dataset at index `datasetIndex`. 29 | * @since 0.1.0 30 | */ 31 | dataset: ChartDataset; 32 | 33 | /** 34 | * Index of the current dataset. 35 | * @since 0.1.0 36 | */ 37 | datasetIndex: number; 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-default.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0], 6 | datasets: [{ 7 | data: [0], 8 | datalabels: { 9 | // labels: undefined 10 | } 11 | }, { 12 | data: [1], 13 | datalabels: { 14 | labels: {} 15 | } 16 | }] 17 | }, 18 | options: { 19 | elements: { 20 | line: { 21 | fill: false 22 | } 23 | }, 24 | layout: { 25 | padding: 16 26 | }, 27 | plugins: { 28 | datalabels: { 29 | backgroundColor: '#f00', 30 | borderColor: '#0f0', 31 | borderWidth: 4, 32 | color: 'transparent', 33 | font: {size: 0}, 34 | padding: 8 35 | } 36 | } 37 | } 38 | }, 39 | options: { 40 | canvas: { 41 | height: 64, 42 | width: 32 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-point-horizontal.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | 3 | ['start', 'center', 'end'].forEach(function(anchor, i) { 4 | datasets.push({ 5 | data: [{x: i, y: i}], 6 | datalabels: { 7 | anchor: anchor 8 | } 9 | }); 10 | }); 11 | 12 | export default { 13 | config: { 14 | type: 'bubble', 15 | data: { 16 | datasets: datasets 17 | }, 18 | options: { 19 | layout: { 20 | padding: 64 21 | }, 22 | elements: { 23 | point: { 24 | radius: 24 25 | } 26 | }, 27 | plugins: { 28 | datalabels: { 29 | backgroundColor: '#00ff77', 30 | borderColor: 'black', 31 | borderWidth: 2, 32 | font: { 33 | size: 0 34 | }, 35 | padding: 8 36 | } 37 | } 38 | } 39 | }, 40 | options: { 41 | canvas: { 42 | height: 256, 43 | width: 256 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-bar-horizontal.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | 3 | ['start', 'center', 'end'].forEach(function(anchor) { 4 | datasets.push({ 5 | data: [0, 4, -4], 6 | datalabels: { 7 | anchor: anchor 8 | } 9 | }); 10 | }); 11 | 12 | export default { 13 | config: { 14 | type: 'bar', 15 | data: { 16 | labels: [0, 1, 2], 17 | datasets: datasets 18 | }, 19 | options: { 20 | indexAxis: 'y', 21 | layout: { 22 | padding: { 23 | left: 20, 24 | right: 20 25 | } 26 | }, 27 | plugins: { 28 | datalabels: { 29 | backgroundColor: '#00ff77', 30 | borderColor: 'black', 31 | borderWidth: 2, 32 | font: { 33 | size: 0 34 | }, 35 | padding: 8 36 | } 37 | } 38 | } 39 | }, 40 | options: { 41 | canvas: { 42 | height: 512, 43 | width: 256 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-indexable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'bar', 4 | data: { 5 | labels: [0, 1, 2, 3], 6 | datasets: [{ 7 | data: [50, 50, -50, -50], 8 | datalabels: { 9 | backgroundColor: '#00ff77' 10 | } 11 | }] 12 | }, 13 | options: { 14 | layout: { 15 | padding: { 16 | top: 20, 17 | bottom: 20 18 | } 19 | }, 20 | scales: { 21 | y: { 22 | min: 25, 23 | max: -25 24 | } 25 | }, 26 | plugins: { 27 | datalabels: { 28 | clamp: [false, true, false, true], 29 | borderColor: 'black', 30 | borderWidth: 2, 31 | font: { 32 | size: 0 33 | }, 34 | offset: 0, 35 | padding: 8 36 | } 37 | } 38 | } 39 | }, 40 | options: { 41 | canvas: { 42 | height: 256, 43 | width: 256 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-values.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var inputs = [-0.5, 0, 0.25, 0.5, 0.75, 1, 1.5]; 3 | 4 | inputs.forEach(function(opacity, j) { 5 | datasets.push({ 6 | data: [{x: j, y: 0}], 7 | datalabels: { 8 | opacity: opacity 9 | } 10 | }); 11 | }); 12 | 13 | export default { 14 | config: { 15 | type: 'bubble', 16 | data: { 17 | datasets: datasets 18 | }, 19 | options: { 20 | layout: { 21 | padding: 24 22 | }, 23 | plugins: { 24 | datalabels: { 25 | backgroundColor: '#00ff77', 26 | borderColor: '#0000ff', 27 | borderWidth: 4, 28 | color: '#0000ff', 29 | font: { 30 | size: 0 31 | }, 32 | padding: 8, 33 | formatter: function() { 34 | return '\u25AE'; 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | options: { 41 | canvas: { 42 | height: 48, 43 | width: 512 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-scriptable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'bar', 4 | data: { 5 | labels: [0, 1, 2, 3], 6 | datasets: [{ 7 | data: [-50, -50, 50, 50], 8 | datalabels: { 9 | backgroundColor: '#00ff77' 10 | } 11 | }] 12 | }, 13 | options: { 14 | layout: { 15 | padding: { 16 | top: 20, 17 | bottom: 20 18 | } 19 | }, 20 | scales: { 21 | y: { 22 | min: 25, 23 | max: -25 24 | } 25 | }, 26 | plugins: { 27 | datalabels: { 28 | clamp: function(context) { 29 | return context.dataIndex % 2 === 0; 30 | }, 31 | borderColor: 'black', 32 | borderWidth: 2, 33 | font: { 34 | size: 0 35 | }, 36 | offset: 0, 37 | padding: 8 38 | } 39 | } 40 | } 41 | }, 42 | options: { 43 | canvas: { 44 | height: 256, 45 | width: 256 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-indexable.js: -------------------------------------------------------------------------------- 1 | var labels = []; 2 | var inputs = [ 3 | 'top', 4 | 'end', 5 | 'left', 6 | 'center', 7 | 'right', 8 | 'start', 9 | 'bottom', 10 | 45, 11 | -45 12 | ]; 13 | 14 | for (var i = 0; i < inputs.length; ++i) { 15 | labels.push(1); 16 | } 17 | 18 | export default { 19 | config: { 20 | type: 'bar', 21 | data: { 22 | labels: labels, 23 | datasets: [{ 24 | data: labels, 25 | datalabels: { 26 | backgroundColor: '#00ff77' 27 | } 28 | }] 29 | }, 30 | options: { 31 | layout: { 32 | padding: 20 33 | }, 34 | plugins: { 35 | datalabels: { 36 | align: inputs, 37 | anchor: 'start', 38 | borderColor: 'black', 39 | borderWidth: 2, 40 | font: { 41 | size: 0 42 | }, 43 | offset: 0, 44 | padding: 8 45 | } 46 | } 47 | } 48 | }, 49 | options: { 50 | canvas: { 51 | height: 128, 52 | width: 320 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-indexable.js: -------------------------------------------------------------------------------- 1 | var labels = []; 2 | var inputs = []; 3 | var count = 20; 4 | 5 | for (var i = 0; i < count; ++i) { 6 | inputs.push(i / count); 7 | labels.push(1); 8 | } 9 | 10 | export default { 11 | config: { 12 | type: 'line', 13 | data: { 14 | labels: labels, 15 | datasets: [{ 16 | data: labels, 17 | datalabels: { 18 | backgroundColor: '#00ff77' 19 | } 20 | }] 21 | }, 22 | options: { 23 | layout: { 24 | padding: 24 25 | }, 26 | plugins: { 27 | datalabels: { 28 | backgroundColor: '#00ff77', 29 | borderColor: '#0000ff', 30 | borderWidth: 4, 31 | color: '#0000ff', 32 | font: { 33 | size: 0 34 | }, 35 | opacity: inputs, 36 | padding: 8, 37 | formatter: function() { 38 | return '\u25AE'; 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | options: { 45 | canvas: { 46 | height: 48, 47 | width: 768 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2021 chartjs-plugin-datalabels contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-angles-radial.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 0, 90, 180, 270, 5 | 45, 70, 160, 520, 6 | -45, -70, -160, -520 7 | ]; 8 | 9 | for (var i = 0; i < inputs.length; ++i) { 10 | labels.push(1); 11 | } 12 | 13 | ['end', 'center', 'start'].forEach(function(anchor) { 14 | datasets.push({ 15 | data: labels, 16 | datalabels: { 17 | align: inputs, 18 | anchor: anchor 19 | } 20 | }); 21 | }); 22 | 23 | export default { 24 | config: { 25 | type: 'doughnut', 26 | data: { 27 | datasets: datasets, 28 | labels: labels 29 | }, 30 | options: { 31 | layout: { 32 | padding: 20 33 | }, 34 | plugins: { 35 | datalabels: { 36 | backgroundColor: '#00ff77', 37 | borderColor: 'black', 38 | borderWidth: 2, 39 | font: { 40 | size: 0 41 | }, 42 | padding: 8, 43 | offset: 0 44 | } 45 | } 46 | } 47 | }, 48 | options: { 49 | canvas: { 50 | height: 512, 51 | width: 512 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-values.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | {x: 0, y: 0}, 3 | {x: 0, y: 1}, 4 | {x: 0, y: 2}, 5 | {x: 1, y: 0}, 6 | {x: 1, y: 1}, 7 | {x: 1, y: 2}, 8 | {x: 2, y: 0}, 9 | {x: 2, y: 1}, 10 | {x: 2, y: 2} 11 | ]; 12 | 13 | export default { 14 | config: { 15 | type: 'bubble', 16 | data: { 17 | datasets: [{ 18 | data: data, 19 | datalabels: { 20 | backgroundColor: '#0f7', 21 | borderColor: 'black', 22 | clip: false, 23 | padding: 16 24 | } 25 | }, { 26 | data: data, 27 | datalabels: { 28 | backgroundColor: '#f07', 29 | borderColor: 'white', 30 | clip: true, 31 | padding: 32 32 | } 33 | }] 34 | }, 35 | options: { 36 | layout: { 37 | padding: 48 38 | }, 39 | plugins: { 40 | datalabels: { 41 | borderWidth: 4, 42 | font: { 43 | size: 0 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | options: { 50 | canvas: { 51 | height: 256, 52 | width: 256 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-arc.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var data = []; 3 | var inputs = [ 4 | 'top', 5 | 'start', 6 | 'left', 7 | 'center', 8 | 'right', 9 | 'end', 10 | 'bottom' 11 | ]; 12 | 13 | for (var i = 0; i < inputs.length; ++i) { 14 | data.push(1); 15 | } 16 | 17 | ['end', 'center', 'start'].forEach(function(anchor) { 18 | datasets.push({ 19 | data: data, 20 | datalabels: { 21 | anchor: anchor, 22 | align: inputs 23 | } 24 | }); 25 | }); 26 | 27 | export default { 28 | config: { 29 | type: 'doughnut', 30 | data: { 31 | datasets: datasets 32 | }, 33 | options: { 34 | cutout: '25%', 35 | layout: { 36 | padding: 24 37 | }, 38 | plugins: { 39 | datalabels: { 40 | backgroundColor: '#00ff77', 41 | borderColor: 'black', 42 | borderWidth: 2, 43 | font: { 44 | size: 0 45 | }, 46 | offset: 0, 47 | padding: 8 48 | } 49 | } 50 | } 51 | }, 52 | options: { 53 | canvas: { 54 | height: 512, 55 | width: 512 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-scriptable.js: -------------------------------------------------------------------------------- 1 | var labels = []; 2 | var inputs = [ 3 | 'top', 4 | 'end', 5 | 'left', 6 | 'center', 7 | 'right', 8 | 'start', 9 | 'bottom', 10 | 45, 11 | -45 12 | ]; 13 | 14 | for (var i = 0; i < inputs.length; ++i) { 15 | labels.push(1); 16 | } 17 | 18 | export default { 19 | config: { 20 | type: 'bar', 21 | data: { 22 | labels: labels, 23 | datasets: [{ 24 | data: labels, 25 | datalabels: { 26 | backgroundColor: '#00ff77' 27 | } 28 | }] 29 | }, 30 | options: { 31 | layout: { 32 | padding: 20 33 | }, 34 | plugins: { 35 | datalabels: { 36 | align: function(context) { 37 | return inputs[context.dataIndex]; 38 | }, 39 | anchor: 'start', 40 | borderColor: 'black', 41 | borderWidth: 2, 42 | font: { 43 | size: 0 44 | }, 45 | offset: 0, 46 | padding: 8 47 | } 48 | } 49 | } 50 | }, 51 | options: { 52 | canvas: { 53 | height: 128, 54 | width: 320 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Highly customizable [Chart.js](https://www.chartjs.org/) plugin that displays labels on data for any type of charts. 4 | 5 | ::: warning COMPATIBILITY NOTE 6 | Requires [Chart.js](https://github.com/chartjs/Chart.js/releases) **3.x** or higher. 7 | ::: 8 | 9 | ::: danger IMPORTANT 10 | This plugin doesn't provide any public API (except the plugin options), thus you should **not** access private properties or methods starting with `$` or `_`, including the `$datalabels` attached property. The implementation can change at any version and could break your production build without notice in upcoming minor/patch releases if any of your code relies on it. 11 | ::: 12 | 13 | ## Table of Contents 14 | 15 | * [Getting Started](getting-started.md) 16 | * [Options](options.md) 17 | * [Labels](labels.md) 18 | * [Positioning](positioning.md) 19 | * [Formatting](formatting.md) 20 | * [Events](events.md) 21 | * [TypeScript](typescript) 22 | * [Migration](migration) 23 | * [Samples](../samples) 24 | 25 | ## License 26 | 27 | `chartjs-plugin-datalabels` is available under the [MIT license](https://github.com/chartjs/chartjs-plugin-datalabels/blob/master/LICENSE.md). 28 | -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-on-top-bar-side.js: -------------------------------------------------------------------------------- 1 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/29 2 | 3 | export default { 4 | config: { 5 | type: 'bar', 6 | data: { 7 | labels: [0], 8 | datasets: [{ 9 | data: [2] 10 | }, { 11 | data: [3] 12 | }, { 13 | data: [4] 14 | }, { 15 | data: [5] 16 | }] 17 | }, 18 | options: { 19 | elements: { 20 | bar: { 21 | backgroundColor: '#444', 22 | } 23 | }, 24 | layout: { 25 | padding: 20 26 | }, 27 | plugins: { 28 | datalabels: { 29 | align: 'end', 30 | anchor: 'start', 31 | backgroundColor: '#fff', 32 | font: { 33 | size: 0 34 | }, 35 | offset: function(context) { 36 | return context.datasetIndex * 42; 37 | }, 38 | padding: { 39 | top: 18, 40 | right: 48, 41 | bottom: 18, 42 | left: 48 43 | } 44 | } 45 | } 46 | } 47 | }, 48 | options: { 49 | canvas: { 50 | height: 256, 51 | width: 256 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-vertical-zero.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 'bottom', 5 | 'start', 6 | 'left', 7 | 'center', 8 | 'right', 9 | 'end', 10 | 'top' 11 | ]; 12 | 13 | for (var i = 0; i < inputs.length; ++i) { 14 | labels.push(0); 15 | } 16 | 17 | ['start', 'center', 'end'].forEach(function(anchor) { 18 | datasets.push({ 19 | data: labels, 20 | datalabels: { 21 | align: inputs, 22 | anchor: anchor 23 | } 24 | }); 25 | }); 26 | 27 | export default { 28 | config: { 29 | type: 'bar', 30 | data: { 31 | datasets: datasets, 32 | labels: labels 33 | }, 34 | options: { 35 | layout: { 36 | padding: { 37 | bottom: 20 38 | } 39 | }, 40 | plugins: { 41 | datalabels: { 42 | backgroundColor: '#00ff77', 43 | borderColor: 'black', 44 | borderWidth: 2, 45 | font: { 46 | size: 0 47 | }, 48 | padding: 8, 49 | offset: 0 50 | } 51 | } 52 | } 53 | }, 54 | options: { 55 | canvas: { 56 | height: 128, 57 | width: 768 58 | } 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-scriptable.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | {x: 0, y: 0}, 3 | {x: 0, y: 1}, 4 | {x: 0, y: 2}, 5 | {x: 1, y: 0}, 6 | {x: 1, y: 1}, 7 | {x: 1, y: 2}, 8 | {x: 2, y: 0}, 9 | {x: 2, y: 1}, 10 | {x: 2, y: 2} 11 | ]; 12 | 13 | export default { 14 | config: { 15 | type: 'bubble', 16 | data: { 17 | datasets: [{ 18 | data: data, 19 | datalabels: { 20 | backgroundColor: '#0f7', 21 | borderColor: 'black', 22 | padding: 16 23 | } 24 | }, { 25 | data: data, 26 | datalabels: { 27 | backgroundColor: '#f07', 28 | borderColor: 'white', 29 | padding: 32 30 | } 31 | }] 32 | }, 33 | options: { 34 | layout: { 35 | padding: 48 36 | }, 37 | plugins: { 38 | datalabels: { 39 | borderWidth: 4, 40 | clip: function(ctx) { 41 | return (ctx.dataIndex + ctx.datasetIndex) % 2 === 1; 42 | }, 43 | font: { 44 | size: 0 45 | } 46 | } 47 | } 48 | } 49 | }, 50 | options: { 51 | canvas: { 52 | height: 256, 53 | width: 256 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-vertical.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 'bottom', 5 | 'start', 6 | 'left', 7 | 'center', 8 | 'right', 9 | 'end', 10 | 'top' 11 | ]; 12 | 13 | for (var i = 0; i < inputs.length; ++i) { 14 | labels.push(1); 15 | } 16 | 17 | ['start', 'center', 'end'].forEach(function(anchor) { 18 | datasets.push({ 19 | data: labels, 20 | datalabels: { 21 | align: inputs, 22 | anchor: anchor 23 | } 24 | }); 25 | }); 26 | 27 | export default { 28 | config: { 29 | type: 'bar', 30 | data: { 31 | datasets: datasets, 32 | labels: labels 33 | }, 34 | options: { 35 | layout: { 36 | padding: { 37 | top: 20, 38 | bottom: 20 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | backgroundColor: '#00ff77', 44 | borderColor: 'black', 45 | borderWidth: 2, 46 | font: { 47 | size: 0 48 | }, 49 | padding: 8, 50 | offset: 0 51 | } 52 | } 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 128, 58 | width: 768 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-horizontal-zero.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 'left', 5 | 'start', 6 | 'top', 7 | 'center', 8 | 'bottom', 9 | 'end', 10 | 'right' 11 | ]; 12 | 13 | for (var i = 0; i < inputs.length; ++i) { 14 | labels.push(0); 15 | } 16 | 17 | ['start', 'center', 'end'].forEach(function(anchor) { 18 | datasets.push({ 19 | data: labels, 20 | datalabels: { 21 | align: inputs, 22 | anchor: anchor 23 | } 24 | }); 25 | }); 26 | 27 | export default { 28 | config: { 29 | type: 'bar', 30 | data: { 31 | datasets: datasets, 32 | labels: labels 33 | }, 34 | options: { 35 | indexAxis: 'y', 36 | layout: { 37 | padding: { 38 | left: 20 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | backgroundColor: '#00ff77', 44 | borderColor: 'black', 45 | borderWidth: 2, 46 | font: { 47 | size: 0 48 | }, 49 | padding: 8, 50 | offset: 0 51 | } 52 | } 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 768, 58 | width: 128 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical-positive.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | 20, 4 | 50, 5 | 80 6 | ]; 7 | 8 | ['start', 'center', 'end'].forEach(function(anchor) { 9 | [false, true].forEach(function(clamp) { 10 | datasets.push({ 11 | data: values, 12 | datalabels: { 13 | anchor: anchor, 14 | clamp: clamp 15 | } 16 | }); 17 | }); 18 | }); 19 | 20 | export default { 21 | config: { 22 | type: 'bar', 23 | data: { 24 | datasets: datasets, 25 | labels: values 26 | }, 27 | options: { 28 | layout: { 29 | padding: { 30 | top: 20, 31 | bottom: 20 32 | } 33 | }, 34 | scales: { 35 | y: { 36 | min: 40, 37 | max: 60 38 | } 39 | }, 40 | plugins: { 41 | datalabels: { 42 | backgroundColor: '#00ff77', 43 | borderColor: 'black', 44 | borderWidth: 2, 45 | font: { 46 | size: 0 47 | }, 48 | padding: 8, 49 | offset: 0 50 | } 51 | } 52 | } 53 | }, 54 | options: { 55 | canvas: { 56 | height: 192, 57 | width: 512 58 | } 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical-negative.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | -20, 4 | -50, 5 | -80 6 | ]; 7 | 8 | ['start', 'center', 'end'].forEach(function(anchor) { 9 | [false, true].forEach(function(clamp) { 10 | datasets.push({ 11 | data: values, 12 | datalabels: { 13 | anchor: anchor, 14 | clamp: clamp 15 | } 16 | }); 17 | }); 18 | }); 19 | 20 | export default { 21 | config: { 22 | type: 'bar', 23 | data: { 24 | datasets: datasets, 25 | labels: values 26 | }, 27 | options: { 28 | layout: { 29 | padding: { 30 | top: 20, 31 | bottom: 20 32 | } 33 | }, 34 | scales: { 35 | y: { 36 | min: -40, 37 | max: -60 38 | } 39 | }, 40 | plugins: { 41 | datalabels: { 42 | backgroundColor: '#00ff77', 43 | borderColor: 'black', 44 | borderWidth: 2, 45 | font: { 46 | size: 0 47 | }, 48 | padding: 8, 49 | offset: 0 50 | } 51 | } 52 | } 53 | }, 54 | options: { 55 | canvas: { 56 | height: 192, 57 | width: 512 58 | } 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-vertical.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | 80, 4 | 20, 5 | -20, 6 | -80 7 | ]; 8 | 9 | ['start', 'center', 'end'].forEach(function(anchor) { 10 | [false, true].forEach(function(clamp) { 11 | datasets.push({ 12 | data: values, 13 | datalabels: { 14 | anchor: anchor, 15 | clamp: clamp 16 | } 17 | }); 18 | }); 19 | }); 20 | 21 | export default { 22 | config: { 23 | type: 'bar', 24 | data: { 25 | datasets: datasets, 26 | labels: values 27 | }, 28 | options: { 29 | layout: { 30 | padding: { 31 | top: 20, 32 | bottom: 20 33 | } 34 | }, 35 | scales: { 36 | y: { 37 | min: 50, 38 | max: -50 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | backgroundColor: '#00ff77', 44 | borderColor: 'black', 45 | borderWidth: 2, 46 | font: { 47 | size: 0 48 | }, 49 | padding: 8, 50 | offset: 0 51 | } 52 | } 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 256, 58 | width: 720 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-on-top-bar-stacked.js: -------------------------------------------------------------------------------- 1 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/32 2 | 3 | export default { 4 | config: { 5 | type: 'bar', 6 | data: { 7 | labels: [0, 1], 8 | datasets: [{ 9 | backgroundColor: '#444', 10 | data: [2, 2] 11 | }, { 12 | backgroundColor: '#222', 13 | data: [2, 2] 14 | }, { 15 | backgroundColor: '#444', 16 | data: [2, 2] 17 | }, { 18 | backgroundColor: '#222', 19 | data: [2, 2] 20 | }] 21 | }, 22 | options: { 23 | elements: { 24 | rectangle: { 25 | borderWidth: 0 26 | } 27 | }, 28 | layout: { 29 | padding: 20 30 | }, 31 | scales: { 32 | x: { 33 | stacked: true 34 | }, 35 | y: { 36 | stacked: true 37 | } 38 | }, 39 | plugins: { 40 | datalabels: { 41 | anchor: 'start', 42 | backgroundColor: '#fff', 43 | font: { 44 | size: 0 45 | }, 46 | padding: 24 47 | } 48 | } 49 | } 50 | }, 51 | options: { 52 | canvas: { 53 | height: 256, 54 | width: 256 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-bar-horizontal.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 'left', 5 | 'start', 6 | 'top', 7 | 'center', 8 | 'bottom', 9 | 'end', 10 | 'right' 11 | ]; 12 | 13 | for (var i = 0; i < inputs.length; ++i) { 14 | labels.push(1); 15 | } 16 | 17 | ['start', 'center', 'end'].forEach(function(anchor) { 18 | datasets.push({ 19 | data: labels, 20 | datalabels: { 21 | align: inputs, 22 | anchor: anchor 23 | } 24 | }); 25 | }); 26 | 27 | export default { 28 | config: { 29 | type: 'bar', 30 | data: { 31 | datasets: datasets, 32 | labels: labels 33 | }, 34 | options: { 35 | indexAxis: 'y', 36 | layout: { 37 | padding: { 38 | left: 20, 39 | right: 20 40 | } 41 | }, 42 | plugins: { 43 | datalabels: { 44 | backgroundColor: '#00ff77', 45 | borderColor: 'black', 46 | borderWidth: 2, 47 | font: { 48 | size: 0 49 | }, 50 | padding: 8, 51 | offset: 0 52 | } 53 | } 54 | } 55 | }, 56 | options: { 57 | canvas: { 58 | height: 768, 59 | width: 128 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /types/test/options.inline.ts: -------------------------------------------------------------------------------- 1 | import {Chart, ChartEvent} from 'chart.js'; 2 | import {Context} from '../context'; 3 | 4 | const chart = new Chart('id', { 5 | type: 'bar', 6 | data: { 7 | labels: [], 8 | datasets: [ 9 | { 10 | data: [], 11 | datalabels: { 12 | align: 'start', 13 | labels: { 14 | foo: {}, 15 | bar: null, 16 | bla: { 17 | align: 'end', 18 | listeners: { 19 | click: (ctx: Context, event: ChartEvent) => true 20 | } 21 | } 22 | }, 23 | listeners: { 24 | click: (ctx: Context, event: ChartEvent) => true 25 | } 26 | } 27 | } 28 | ] 29 | }, 30 | options: { 31 | plugins: { 32 | datalabels: { 33 | align: 'start', 34 | labels: { 35 | foo: {}, 36 | bar: null, 37 | bla: { 38 | align: 'end', 39 | listeners: { 40 | click: (ctx: Context, event: ChartEvent) => true 41 | } 42 | } 43 | }, 44 | listeners: { 45 | click: (ctx: Context, event: ChartEvent) => true 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /scripts/attach-gh-assets.js: -------------------------------------------------------------------------------- 1 | // https://hub.github.com/hub-release.1.html 2 | // https://cli.github.com/manual/ 3 | /* eslint-disable no-process-exit */ 4 | 5 | const {run} = require('./utils'); 6 | const pkg = require('../package.json'); 7 | 8 | const TAG = `v${pkg.version}`; 9 | 10 | const ASSETS = [ 11 | `dist/${pkg.name}.js`, 12 | `dist/${pkg.name}.min.js`, 13 | `dist/${pkg.name}.esm.js`, 14 | `dist/${pkg.name}.tgz`, 15 | `dist/${pkg.name}.zip`, 16 | ]; 17 | 18 | (async() => { 19 | if (!process.env.GITHUB_TOKEN) { 20 | throw new Error('GITHUB_TOKEN environment variable required'); 21 | } 22 | if ((await run(`hub release show ${TAG} -f "%T: %S"`)) !== `${TAG}: draft`) { 23 | throw new Error(`Release ${TAG} has already been published.`); 24 | } 25 | 26 | // Attach assets to the associated GitHub release. 27 | // Note that the `hub release edit -a ${ASSETS.join(' -a ')} -m "" "${TAG}"` 28 | // command does not allow to overwrite existing assets, so use 'gh' instead. 29 | // See https://github.com/github/hub/issues/2657 30 | await run(`gh release upload ${TAG} ${ASSETS.join(' ')} --clobber`); 31 | 32 | })().catch((error) => { 33 | console.error(`Failed to publish to github: ${error.message || error}.`); 34 | process.exit(1); 35 | }); 36 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal-positive.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | 20, 4 | 50, 5 | 80 6 | ]; 7 | 8 | ['start', 'center', 'end'].forEach(function(anchor) { 9 | [false, true].forEach(function(clamp) { 10 | datasets.push({ 11 | data: values, 12 | datalabels: { 13 | anchor: anchor, 14 | clamp: clamp 15 | } 16 | }); 17 | }); 18 | }); 19 | 20 | export default { 21 | config: { 22 | type: 'bar', 23 | data: { 24 | datasets: datasets, 25 | labels: values 26 | }, 27 | options: { 28 | indexAxis: 'y', 29 | layout: { 30 | padding: { 31 | left: 20, 32 | right: 20 33 | } 34 | }, 35 | scales: { 36 | x: { 37 | min: 40, 38 | max: 60 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | backgroundColor: '#00ff77', 44 | borderColor: 'black', 45 | borderWidth: 2, 46 | font: { 47 | size: 0 48 | }, 49 | padding: 8, 50 | offset: 0 51 | } 52 | } 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 512, 58 | width: 192 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal-negative.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | -20, 4 | -50, 5 | -80 6 | ]; 7 | 8 | ['start', 'center', 'end'].forEach(function(anchor) { 9 | [false, true].forEach(function(clamp) { 10 | datasets.push({ 11 | data: values, 12 | datalabels: { 13 | anchor: anchor, 14 | clamp: clamp 15 | } 16 | }); 17 | }); 18 | }); 19 | 20 | export default { 21 | config: { 22 | type: 'bar', 23 | data: { 24 | datasets: datasets, 25 | labels: values 26 | }, 27 | options: { 28 | indexAxis: 'y', 29 | layout: { 30 | padding: { 31 | left: 20, 32 | right: 20 33 | } 34 | }, 35 | scales: { 36 | x: { 37 | min: -60, 38 | max: -40 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | backgroundColor: '#00ff77', 44 | borderColor: 'black', 45 | borderWidth: 2, 46 | font: { 47 | size: 0 48 | }, 49 | padding: 8, 50 | offset: 0 51 | } 52 | } 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 512, 58 | width: 192 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/options.clamp/clamp-values-bar-horizontal.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var values = [ 3 | 80, 4 | 20, 5 | -20, 6 | -80 7 | ]; 8 | 9 | ['start', 'center', 'end'].forEach(function(anchor) { 10 | [false, true].forEach(function(clamp) { 11 | datasets.push({ 12 | data: values, 13 | datalabels: { 14 | anchor: anchor, 15 | clamp: clamp 16 | } 17 | }); 18 | }); 19 | }); 20 | 21 | export default { 22 | config: { 23 | type: 'bar', 24 | data: { 25 | datasets: datasets, 26 | labels: values 27 | }, 28 | options: { 29 | indexAxis: 'y', 30 | layout: { 31 | padding: { 32 | left: 20, 33 | right: 20 34 | } 35 | }, 36 | scales: { 37 | x: { 38 | min: 50, 39 | max: -50 40 | } 41 | }, 42 | plugins: { 43 | datalabels: { 44 | backgroundColor: '#00ff77', 45 | borderColor: 'black', 46 | borderWidth: 2, 47 | font: { 48 | size: 0 49 | }, 50 | padding: 8, 51 | offset: 0 52 | } 53 | } 54 | } 55 | }, 56 | options: { 57 | canvas: { 58 | height: 720, 59 | width: 256 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-angles-linear.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var inputs = [ 4 | 0, 90, 180, 270, 5 | 45, 70, 160, 520, 6 | -45, -70, -160, -520 7 | ]; 8 | 9 | for (var i = 0; i < inputs.length; ++i) { 10 | labels.push(1); 11 | } 12 | 13 | ['start', 'center', 'end'].forEach(function(anchor) { 14 | datasets.push({ 15 | data: labels, 16 | datalabels: { 17 | align: inputs, 18 | anchor: anchor 19 | } 20 | }); 21 | }); 22 | 23 | export default { 24 | config: { 25 | type: 'bar', 26 | data: { 27 | datasets: datasets, 28 | labels: labels 29 | }, 30 | options: { 31 | layout: { 32 | padding: 20 33 | }, 34 | scales: { 35 | x: { 36 | stacked: true 37 | }, 38 | y: { 39 | stacked: true 40 | } 41 | }, 42 | plugins: { 43 | datalabels: { 44 | backgroundColor: '#00ff77', 45 | borderColor: 'black', 46 | borderWidth: 2, 47 | font: { 48 | size: 0 49 | }, 50 | padding: 8, 51 | offset: 0 52 | } 53 | } 54 | } 55 | }, 56 | options: { 57 | canvas: { 58 | height: 256, 59 | width: 512 60 | } 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /test/fixtures/options.opacity/opacity-scriptable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 6 | datasets: [{ 7 | data: [0, 100, 10, 90, 20, 80, 30, 70, 40, 60, 50], 8 | datalabels: { 9 | backgroundColor: '#00ff77' 10 | } 11 | }] 12 | }, 13 | options: { 14 | layout: { 15 | padding: 24 16 | }, 17 | elements: { 18 | line: { 19 | backgroundColor: 'transparent', 20 | borderColor: 'transparent' 21 | } 22 | }, 23 | plugins: { 24 | datalabels: { 25 | backgroundColor: '#00ff77', 26 | borderColor: '#0000ff', 27 | borderWidth: 4, 28 | color: '#0000ff', 29 | font: { 30 | size: 0 31 | }, 32 | opacity: function(context) { 33 | var data = context.dataset.data[context.dataIndex]; 34 | return data / 100; 35 | }, 36 | padding: 8, 37 | formatter: function() { 38 | return '\u25AE'; 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | options: { 45 | canvas: { 46 | height: 64, 47 | width: 512 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const {exec} = require('child_process'); 2 | const semver = require('semver'); 3 | 4 | module.exports = { 5 | run: (cmd) => new Promise((resolve, reject) => { 6 | exec(cmd, {}, (error, stdout, stderr) => { 7 | if (error) { 8 | reject(stderr.trim()); 9 | } else { 10 | resolve(stdout.trim()); 11 | } 12 | }); 13 | }), 14 | 15 | /** 16 | * Returns, based on existing npm `tags`, the tag under which to publish the given `version`. 17 | */ 18 | semtag: (tags, version) => { 19 | const {latest} = tags; 20 | 21 | // Versions prior to 'latest' are marked 'untagged'. 22 | if (latest && semver.gte(latest, version)) { 23 | return 'untagged'; 24 | } 25 | 26 | // Full versions greater than 'latest' become the new 'latest'. 27 | if (!semver.prerelease(version)) { 28 | return 'latest'; 29 | } 30 | 31 | // Pre-versions for the same 'latest' major version are tagged 'dev', else are 32 | // tagged 'next' for a greater major version. However, if the tag already exists 33 | // with a greater version, the version to publish is marked 'untagged'. 34 | const tag = latest && semver.major(latest) < semver.major(version) ? 'next' : 'dev'; 35 | if (tags[tag] && semver.lte(version, tags[tag])) { 36 | return 'untagged'; 37 | } 38 | 39 | return tag; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /scripts/create-release-tag.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | 3 | const {run} = require('./utils'); 4 | const pkg = require('../package.json'); 5 | 6 | const TAG = `v${pkg.version}`; 7 | const TAG_FILES = [ 8 | `dist/${pkg.name}.js`, 9 | `dist/${pkg.name}.min.js`, 10 | `dist/${pkg.name}.esm.js`, 11 | 'bower.json', 12 | ]; 13 | 14 | (async() => { 15 | if (!process.env.GH_AUTH_EMAIL) { 16 | throw new Error('GH_AUTH_EMAIL environment variable required'); 17 | } 18 | if (!process.env.GH_AUTH_NAME) { 19 | throw new Error('GH_AUTH_NAME environment variable required'); 20 | } 21 | if (await run(`git ls-remote origin refs/tags/${TAG}`)) { 22 | throw new Error(`Git tag ${TAG} already exists`); 23 | } 24 | 25 | // Tag a detached branch containing the dist (and bower) files. 26 | await run(`git config --global user.email "${process.env.GH_AUTH_EMAIL}"`); 27 | await run(`git config --global user.name "${process.env.GH_AUTH_NAME}"`); 28 | await run('git checkout --detach --quiet'); 29 | await run(`git add -f ${TAG_FILES.join(' ')}`); 30 | await run(`git commit -m "Release ${TAG}"`); 31 | await run(`git tag -a "${TAG}" -m "Version ${pkg.version}"`); 32 | await run(`git push origin refs/tags/${TAG}`); 33 | 34 | })().catch((error) => { 35 | console.error(`Failed to create release tag: ${error.message || error}.`); 36 | process.exit(1); 37 | }); 38 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-point-radial.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var count = 12; 4 | 5 | for (var i = 0; i < count; ++i) { 6 | labels.push(i); 7 | } 8 | 9 | ['start', 'center', 'end'].forEach(function(anchor, v) { 10 | datasets.push({ 11 | data: labels.map(function() { 12 | return v + 2; 13 | }), 14 | datalabels: { 15 | anchor: anchor 16 | } 17 | }); 18 | }); 19 | 20 | export default { 21 | config: { 22 | type: 'radar', 23 | data: { 24 | labels: labels, 25 | datasets: datasets 26 | }, 27 | options: { 28 | layout: { 29 | padding: 32 30 | }, 31 | elements: { 32 | line: { 33 | backgroundColor: 'transparent', 34 | borderColor: 'transparent' 35 | }, 36 | point: { 37 | radius: 16 38 | } 39 | }, 40 | scales: { 41 | r: { 42 | min: 0, 43 | display: false 44 | } 45 | }, 46 | plugins: { 47 | datalabels: { 48 | backgroundColor: '#00ff77', 49 | borderColor: 'black', 50 | borderWidth: 2, 51 | font: { 52 | size: 0 53 | }, 54 | padding: 8 55 | } 56 | } 57 | } 58 | }, 59 | options: { 60 | canvas: { 61 | height: 512, 62 | width: 512 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-point-horizontal.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var inputs = [ 3 | 'bottom', 4 | 'start', 5 | 'left', 6 | 'center', 7 | 'right', 8 | 'end', 9 | 'top' 10 | ]; 11 | 12 | ['start', 'center', 'end'].forEach(function(anchor, i) { 13 | inputs.forEach(function(align, j) { 14 | datasets.push({ 15 | data: [{x: i * 2, y: j * 2}], 16 | datalabels: { 17 | align: align, 18 | anchor: anchor 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | export default { 25 | config: { 26 | type: 'bubble', 27 | data: { 28 | datasets: datasets 29 | }, 30 | options: { 31 | layout: { 32 | padding: 64 33 | }, 34 | elements: { 35 | point: { 36 | radius: 24 37 | } 38 | }, 39 | plugins: { 40 | datalabels: { 41 | backgroundColor: '#00ff77', 42 | borderColor: 'black', 43 | borderWidth: 2, 44 | color: 'transparent', 45 | padding: { 46 | top: 2, 47 | bottom: 2, 48 | left: 8, 49 | right: 8 50 | }, 51 | offset: 0, 52 | formatter: function(v) { 53 | return v !== null ? '' : null; 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | options: { 60 | canvas: { 61 | height: 512, 62 | width: 512 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const terser = require('rollup-plugin-terser').terser; 2 | const pkg = require('./package.json'); 3 | 4 | const banner = `/*! 5 | * ${pkg.name} v${pkg.version} 6 | * ${pkg.homepage} 7 | * (c) 2017-${new Date().getFullYear()} ${pkg.name} contributors 8 | * Released under the ${pkg.license} license 9 | */`; 10 | 11 | module.exports = [ 12 | { 13 | input: 'src/plugin.js', 14 | output: ['.js', '.min.js'].map((suffix) => { 15 | const config = { 16 | name: 'ChartDataLabels', 17 | file: `dist/${pkg.name}${suffix}`, 18 | banner: banner, 19 | format: 'umd', 20 | indent: false, 21 | plugins: [], 22 | globals: { 23 | 'chart.js': 'Chart', 24 | 'chart.js/helpers': 'Chart.helpers' 25 | } 26 | }; 27 | 28 | if (suffix.match(/\.min\.js$/)) { 29 | config.plugins.push( 30 | terser({ 31 | output: { 32 | comments: /^!/ 33 | } 34 | }) 35 | ); 36 | } 37 | 38 | return config; 39 | }), 40 | external: [ 41 | 'chart.js', 42 | 'chart.js/helpers', 43 | ] 44 | }, 45 | { 46 | input: 'src/plugin.js', 47 | output: { 48 | file: pkg.module, 49 | banner: banner, 50 | format: 'esm', 51 | indent: false 52 | }, 53 | external: [ 54 | 'chart.js', 55 | 'chart.js/helpers', 56 | ] 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /scripts/create-packages.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | 3 | const archiver = require('archiver'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const pkg = require('../package.json'); 7 | 8 | const root = path.resolve(__dirname, '..'); 9 | const inputs = [ 10 | path.join(root, 'dist', `${pkg.name}.js`), 11 | path.join(root, 'dist', `${pkg.name}.min.js`), 12 | path.join(root, 'dist', `${pkg.name}.esm.js`), 13 | path.join(root, 'LICENSE.md'), 14 | path.join(root, 'README.md'), 15 | ]; 16 | 17 | const targets = [ 18 | {format: 'tar', ext: 'tgz', options: {gzip: true}}, 19 | {format: 'zip', ext: 'zip', options: {}}, 20 | ]; 21 | 22 | (async() => { 23 | for (const input of inputs) { 24 | if (!fs.existsSync(input)) { 25 | throw new Error( 26 | `The file "${path.relative(root, input)}" does not ` + 27 | 'exists: make sure to execute "npm run build" first'); 28 | } 29 | } 30 | 31 | await Promise.all(targets.map((target) => { 32 | const dest = path.join(root, 'dist', `${pkg.name}.${target.ext}`); 33 | const archive = archiver(target.format, target.options); 34 | for (const input of inputs) { 35 | archive.file(input, {name: path.basename(input)}); 36 | } 37 | 38 | archive.pipe(fs.createWriteStream(dest)); 39 | return archive.finalize(); 40 | })); 41 | })().catch((error) => { 42 | console.error(`Failed to create packages: ${error.message || error}.`); 43 | process.exit(1); 44 | }); 45 | -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-indexable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3], 6 | datasets: [{ 7 | data: [1, 1, 1, 1], 8 | datalabels: { 9 | labels: { 10 | foo: { 11 | borderColor: [ 12 | '#f00', 13 | '#0f0', 14 | '#00f', 15 | '#ff0', 16 | ], 17 | borderWidth: [ 18 | 10, 19 | 8, 20 | 6, 21 | 4 22 | ], 23 | padding: [ 24 | 4, 25 | 6, 26 | 8, 27 | 10 28 | ] 29 | } 30 | } 31 | } 32 | }] 33 | }, 34 | options: { 35 | scales: { 36 | y: { 37 | beginAtZero: true 38 | } 39 | }, 40 | elements: { 41 | line: { 42 | fill: false 43 | } 44 | }, 45 | layout: { 46 | padding: 16 47 | }, 48 | plugins: { 49 | datalabels: { 50 | backgroundColor: '#000', 51 | borderColor: '#fff', 52 | borderWidth: 4, 53 | color: 'transparent', 54 | font: {size: 0}, 55 | padding: 8 56 | } 57 | } 58 | } 59 | }, 60 | options: { 61 | canvas: { 62 | height: 64, 63 | width: 128 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /test/fixtures/options.display/display-scriptable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 6 | datasets: [{ 7 | data: [4, 2, 0, -2, -4, -2, 0, 2, 4, 2], 8 | datalabels: { 9 | backgroundColor: '#f00', 10 | // display: (fallback) 11 | } 12 | }, { 13 | data: [-4, -2, 0, 2, 4, 2, 0, -2, -4, -2], 14 | datalabels: { 15 | backgroundColor: '#0f0', 16 | // display: (fallback) 17 | } 18 | }] 19 | }, 20 | options: { 21 | layout: { 22 | padding: 64 23 | }, 24 | elements: { 25 | line: { 26 | borderColor: 'transparent', 27 | fill: false 28 | } 29 | }, 30 | plugins: { 31 | datalabels: { 32 | borderColor: '#0000ff', 33 | borderWidth: 4, 34 | display(ctx) { 35 | return ctx.datasetIndex === 0 36 | ? ctx.dataIndex % 2 === 0 37 | : 'auto'; 38 | }, 39 | font: { 40 | size: 0 41 | }, 42 | padding: { 43 | top: 16, 44 | right: 24, 45 | bottom: 16, 46 | left: 24 47 | }, 48 | rotation: (ctx) => ctx.dataIndex * 42 49 | } 50 | } 51 | } 52 | }, 53 | options: { 54 | canvas: { 55 | height: 320, 56 | width: 320 57 | } 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /test/fixtures/drawing/draw-skipped-values-in-correct-order.js: -------------------------------------------------------------------------------- 1 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/319 2 | 3 | export default { 4 | config: { 5 | type: 'radar', 6 | data: { 7 | labels: ['1', '2', '3', '4', '5'], 8 | datasets: [ 9 | { 10 | label: 'Series 1', 11 | data: [5, 5, 5, 5, 5], 12 | backgroundColor: 'red', 13 | borderWidth: 0, 14 | fill: false, 15 | borderColor: 'red', 16 | datalabels: { 17 | backgroundColor: 'red', 18 | } 19 | }, 20 | { 21 | label: 'Series 2', 22 | data: [4.5, 4.5, null, 4.5, 4.5], 23 | backgroundColor: 'blue', 24 | borderWidth: 0, 25 | fill: false, 26 | borderColor: 'blue', 27 | datalabels: { 28 | backgroundColor: 'blue', 29 | } 30 | } 31 | ] 32 | }, 33 | options: { 34 | layout: { 35 | padding: 20 36 | }, 37 | scales: { 38 | r: { 39 | display: false, 40 | min: 1, 41 | max: 5 42 | }, 43 | }, 44 | plugins: { 45 | datalabels: { 46 | font: { 47 | size: 0 48 | }, 49 | padding: 16, 50 | } 51 | } 52 | 53 | } 54 | }, 55 | options: { 56 | canvas: { 57 | height: 256, 58 | width: 256 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/options.align/align-presets-point-radial.js: -------------------------------------------------------------------------------- 1 | var datasets = []; 2 | var labels = []; 3 | var count = 7; 4 | var inputs = [ 5 | 'bottom', 6 | 'start', 7 | 'left', 8 | 'center', 9 | 'right', 10 | 'end', 11 | 'top' 12 | ]; 13 | 14 | for (var i = 0; i < count; ++i) { 15 | labels.push(i); 16 | } 17 | 18 | ['start', 'center', 'end'].forEach(function(anchor, v) { 19 | datasets.push({ 20 | data: labels.map(function() { 21 | return v + 2; 22 | }), 23 | datalabels: { 24 | align: inputs, 25 | anchor: anchor 26 | } 27 | }); 28 | }); 29 | 30 | export default { 31 | config: { 32 | type: 'radar', 33 | data: { 34 | labels: labels, 35 | datasets: datasets 36 | }, 37 | options: { 38 | layout: { 39 | padding: 32 40 | }, 41 | elements: { 42 | line: { 43 | backgroundColor: 'transparent', 44 | borderColor: 'transparent' 45 | }, 46 | point: { 47 | radius: 16 48 | } 49 | }, 50 | scales: { 51 | r: { 52 | min: 0, 53 | display: false 54 | } 55 | }, 56 | plugins: { 57 | datalabels: { 58 | backgroundColor: '#00ff77', 59 | borderColor: 'black', 60 | borderWidth: 2, 61 | font: { 62 | size: 0 63 | }, 64 | offset: 0, 65 | padding: 8 66 | } 67 | } 68 | } 69 | }, 70 | options: { 71 | canvas: { 72 | height: 512, 73 | width: 512 74 | } 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /test/fixtures/options.clip/clip-indexable.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | {x: 0, y: 0}, 3 | {x: 0, y: 1}, 4 | {x: 0, y: 2}, 5 | {x: 1, y: 0}, 6 | {x: 1, y: 1}, 7 | {x: 1, y: 2}, 8 | {x: 2, y: 0}, 9 | {x: 2, y: 1}, 10 | {x: 2, y: 2} 11 | ]; 12 | 13 | export default { 14 | config: { 15 | type: 'bubble', 16 | data: { 17 | datasets: [{ 18 | data: data, 19 | datalabels: { 20 | backgroundColor: '#0f7', 21 | borderColor: 'black', 22 | padding: 16, 23 | clip: [ 24 | true, 25 | false, 26 | true, 27 | false, 28 | true, 29 | false, 30 | true, 31 | false, 32 | true, 33 | ] 34 | } 35 | }, { 36 | data: data, 37 | datalabels: { 38 | backgroundColor: '#f07', 39 | borderColor: 'white', 40 | padding: 32, 41 | clip: [ 42 | false, 43 | true, 44 | false, 45 | true, 46 | false, 47 | true, 48 | false, 49 | true, 50 | false 51 | ] 52 | } 53 | }] 54 | }, 55 | options: { 56 | layout: { 57 | padding: 48 58 | }, 59 | plugins: { 60 | datalabels: { 61 | borderWidth: 4, 62 | font: { 63 | size: 0 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | options: { 70 | canvas: { 71 | height: 256, 72 | width: 256 73 | } 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | import {isNullOrUndef, isObject} from 'chart.js/helpers'; 2 | 3 | var formatter = function(value) { 4 | if (isNullOrUndef(value)) { 5 | return null; 6 | } 7 | 8 | var label = value; 9 | var keys, klen, k; 10 | if (isObject(value)) { 11 | if (!isNullOrUndef(value.label)) { 12 | label = value.label; 13 | } else if (!isNullOrUndef(value.r)) { 14 | label = value.r; 15 | } else { 16 | label = ''; 17 | keys = Object.keys(value); 18 | for (k = 0, klen = keys.length; k < klen; ++k) { 19 | label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]]; 20 | } 21 | } 22 | } 23 | 24 | return '' + label; 25 | }; 26 | 27 | /** 28 | * IMPORTANT: make sure to also update tests and TypeScript definition 29 | * files (`/test/specs/defaults.spec.js` and `/types/options.d.ts`) 30 | */ 31 | 32 | export default { 33 | align: 'center', 34 | anchor: 'center', 35 | backgroundColor: null, 36 | borderColor: null, 37 | borderRadius: 0, 38 | borderWidth: 0, 39 | clamp: false, 40 | clip: false, 41 | color: undefined, 42 | display: true, 43 | font: { 44 | family: undefined, 45 | lineHeight: 1.2, 46 | size: undefined, 47 | style: undefined, 48 | weight: null 49 | }, 50 | formatter: formatter, 51 | labels: undefined, 52 | listeners: {}, 53 | offset: 4, 54 | opacity: 1, 55 | padding: { 56 | top: 4, 57 | right: 4, 58 | bottom: 4, 59 | left: 4 60 | }, 61 | rotation: 0, 62 | textAlign: 'start', 63 | textStrokeColor: undefined, 64 | textStrokeWidth: 0, 65 | textShadowBlur: 0, 66 | textShadowColor: undefined 67 | }; 68 | -------------------------------------------------------------------------------- /test/fixtures/options.display/display-values-bool.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 6 | datasets: [{ 7 | data: [4, 3, 2, 1, 0, 0, -1, -2, -3, -4], 8 | datalabels: { 9 | backgroundColor: '#ff0', 10 | display: false 11 | } 12 | }, { 13 | data: [4, 2, 0, -2, -4, -2, 0, 2, 4, 2], 14 | datalabels: { 15 | backgroundColor: '#0f0', 16 | display: true 17 | } 18 | }, { 19 | data: [-4, -2, 0, 2, 4, 2, 0, -2, -4, -2], 20 | datalabels: { 21 | backgroundColor: '#f00', 22 | display: true 23 | } 24 | }, { 25 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 26 | datalabels: { 27 | backgroundColor: '#f0f', 28 | // display: (fallback: true) 29 | } 30 | }] 31 | }, 32 | options: { 33 | layout: { 34 | padding: 64 35 | }, 36 | elements: { 37 | line: { 38 | borderColor: 'transparent', 39 | fill: false 40 | } 41 | }, 42 | plugins: { 43 | datalabels: { 44 | borderColor: '#0000ff', 45 | borderWidth: 4, 46 | font: { 47 | size: 0 48 | }, 49 | padding: { 50 | top: 16, 51 | right: 24, 52 | bottom: 16, 53 | left: 24 54 | }, 55 | rotation: (ctx) => ctx.dataIndex * 42 56 | } 57 | } 58 | } 59 | }, 60 | options: { 61 | canvas: { 62 | height: 320, 63 | width: 320 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /test/fixtures/options.display/display-values-auto.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 6 | datasets: [{ 7 | data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 8 | datalabels: { 9 | backgroundColor: '#f0f', 10 | display: true 11 | } 12 | }, { 13 | data: [-4, -2, 0, 2, 4, 2, 0, -2, -4, -2, 0], 14 | datalabels: { 15 | backgroundColor: '#f00', 16 | // display: (fallback: auto) 17 | } 18 | }, { 19 | data: [4, 2, 0, -2, -4, -2, 0, 2, 4, 2, 0], 20 | datalabels: { 21 | backgroundColor: '#0f0', 22 | display: 'auto' 23 | } 24 | }, { 25 | data: [4, 3, 2, 1, 0, 0, -1, -2, -3, -4], 26 | datalabels: { 27 | backgroundColor: '#ff0', 28 | display: false 29 | } 30 | }] 31 | }, 32 | options: { 33 | layout: { 34 | padding: 64 35 | }, 36 | elements: { 37 | line: { 38 | borderColor: 'transparent', 39 | fill: false 40 | } 41 | }, 42 | plugins: { 43 | datalabels: { 44 | borderColor: '#0000ff', 45 | borderWidth: 4, 46 | display: 'auto', 47 | font: { 48 | size: 0 49 | }, 50 | padding: { 51 | top: 16, 52 | right: 24, 53 | bottom: 16, 54 | left: 24 55 | }, 56 | rotation: (ctx) => 45 + ctx.dataIndex * 22.5 57 | } 58 | } 59 | } 60 | }, 61 | options: { 62 | canvas: { 63 | height: 320, 64 | width: 320 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /test/fixtures/options.display/display-indexable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 6 | datasets: [{ 7 | data: [4, 2, 0, -2, -4, -2, 0, 2, 4, 2], 8 | datalabels: { 9 | backgroundColor: '#f00', 10 | display: [ 11 | true, 12 | true, 13 | true, 14 | false, 15 | false, 16 | 'auto', 17 | 'auto', 18 | false, 19 | 'auto', 20 | 'auto', 21 | ] 22 | } 23 | }, { 24 | data: [-4, -2, 0, 2, 4, 2, 0, -2, -4, -2], 25 | datalabels: { 26 | backgroundColor: '#0f0', 27 | // display: (fallback) 28 | } 29 | }] 30 | }, 31 | options: { 32 | layout: { 33 | padding: 64 34 | }, 35 | elements: { 36 | line: { 37 | borderColor: 'transparent', 38 | fill: false 39 | } 40 | }, 41 | plugins: { 42 | datalabels: { 43 | borderColor: '#0000ff', 44 | borderWidth: 4, 45 | display: [ 46 | 'auto', 47 | 'auto', 48 | false, 49 | 'auto', 50 | 'auto', 51 | false, 52 | false, 53 | true, 54 | true, 55 | true 56 | ], 57 | font: { 58 | size: 0 59 | }, 60 | padding: { 61 | top: 16, 62 | right: 24, 63 | bottom: 16, 64 | left: 24 65 | }, 66 | rotation: (ctx) => ctx.dataIndex * 42 67 | } 68 | } 69 | } 70 | }, 71 | options: { 72 | canvas: { 73 | height: 320, 74 | width: 320 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-scriptable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0, 1, 2, 3], 6 | datasets: [{ 7 | data: [1, 1, 1, 1], 8 | datalabels: { 9 | labels: { 10 | foo: { 11 | borderColor(ctx) { 12 | switch (ctx.dataIndex) { 13 | case 0: return '#f00'; 14 | case 1: return '#0f0'; 15 | case 2: return '#00f'; 16 | default: return '#ff0'; 17 | } 18 | }, 19 | borderWidth(ctx) { 20 | switch (ctx.dataIndex) { 21 | case 0: return 10; 22 | case 1: return 8; 23 | case 2: return 6; 24 | default: return 4; 25 | } 26 | }, 27 | padding(ctx) { 28 | switch (ctx.dataIndex) { 29 | case 0: return 4; 30 | case 1: return 6; 31 | case 2: return 8; 32 | default: return 10; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }] 39 | }, 40 | options: { 41 | scales: { 42 | y: { 43 | beginAtZero: true 44 | } 45 | }, 46 | elements: { 47 | line: { 48 | fill: false 49 | } 50 | }, 51 | layout: { 52 | padding: 16 53 | }, 54 | plugins: { 55 | datalabels: { 56 | backgroundColor: '#000', 57 | borderColor: '#fff', 58 | borderWidth: 4, 59 | color: 'transparent', 60 | font: {size: 0}, 61 | padding: 8 62 | } 63 | } 64 | } 65 | }, 66 | options: { 67 | canvas: { 68 | height: 64, 69 | width: 128 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /types/test/options.indexable.ts: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import {Options} from '../options'; 3 | 4 | const options: Options = { 5 | align: ['bottom', 'center', 'end', 'left', 'right', 'start', 'top', 42], 6 | anchor: ['center', 'end', 'start'], 7 | backgroundColor: ['blue', 'red', null], 8 | borderColor: ['blue', 'red', null], 9 | borderRadius: [0, 42], 10 | borderWidth: [0, 42], 11 | clamp: [false, true], 12 | clip: [false, true], 13 | color: ['blue', 'red'], 14 | display: [false, true], 15 | font: [{size: 42}, {weight: 200}, {lineHeight: '20%'}], 16 | labels: { 17 | foo: {}, 18 | bar: null, 19 | bla: { 20 | align: ['bottom', 'center', 'end', 'left', 'right', 'start', 'top', 42], 21 | anchor: ['center', 'end', 'start'], 22 | backgroundColor: ['blue', 'red', null], 23 | borderColor: ['blue', 'red', null], 24 | borderRadius: [0, 42], 25 | borderWidth: [0, 42], 26 | clamp: [false, true], 27 | clip: [false, true], 28 | color: ['blue', 'red'], 29 | display: [false, true], 30 | font: [{size: 42}, {weight: 200}, {lineHeight: '20%'}], 31 | offset: [0, 42], 32 | opacity: [0, 0.42], 33 | padding: [0, 42, {top: 42}], 34 | rotation: [0, 42], 35 | textAlign: ['center', 'end', 'start', 'left', 'right'], 36 | textStrokeColor: ['blue', 'red'], 37 | textStrokeWidth: [0, 42], 38 | textShadowBlur: [0, 42], 39 | textShadowColor: ['blue', 'red'] 40 | } 41 | }, 42 | offset: [0, 42], 43 | opacity: [0, 0.42], 44 | padding: [0, 42, {top: 42}], 45 | rotation: [0, 42], 46 | textAlign: ['center', 'end', 'start', 'left', 'right'], 47 | textStrokeColor: ['blue', 'red'], 48 | textStrokeWidth: [0, 42], 49 | textShadowBlur: [0, 42], 50 | textShadowColor: ['blue', 'red'] 51 | }; 52 | 53 | const chart = new Chart('id', { 54 | type: 'bar', 55 | data: { 56 | labels: [], 57 | datasets: [ 58 | { 59 | data: [], 60 | datalabels: options 61 | } 62 | ] 63 | }, 64 | options: { 65 | plugins: { 66 | datalabels: options 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /test/fixtures/options.labels/labels-value.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'line', 4 | data: { 5 | labels: [0], 6 | datasets: [{ 7 | data: [0], 8 | datalabels: { 9 | // Fallback to global `labels` option. 10 | // labels: undefined 11 | } 12 | }, { 13 | data: [1], 14 | datalabels: { 15 | // Fallback to global `labels` option. 16 | labels: {} 17 | } 18 | }, { 19 | data: [2], 20 | datalabels: { 21 | // Overrides global `labels`. 22 | labels: { 23 | foo: { 24 | backgroundColor: '#800' 25 | }, 26 | bar: { 27 | backgroundColor: '#080' 28 | } 29 | } 30 | } 31 | }, { 32 | data: [3], 33 | datalabels: { 34 | // Removes the `bar` label. 35 | labels: { 36 | bar: null 37 | } 38 | } 39 | }, { 40 | data: [4], 41 | datalabels: { 42 | // Removes all labels 43 | labels: { 44 | foo: null, 45 | bar: null 46 | } 47 | } 48 | }] 49 | }, 50 | options: { 51 | elements: { 52 | line: { 53 | fill: false 54 | } 55 | }, 56 | layout: { 57 | padding: 16 58 | }, 59 | plugins: { 60 | datalabels: { 61 | backgroundColor: '#f00', 62 | borderColor: '#0f0', 63 | borderWidth: 4, 64 | color: 'transparent', 65 | font: {size: 0}, 66 | padding: 8, 67 | labels: { 68 | foo: { 69 | backgroundColor: '#00f', 70 | borderColor: '#f0f' 71 | }, 72 | bar: { 73 | align: 'right', 74 | backgroundColor: '#0ff', 75 | borderColor: '#fff', 76 | offset: 20 77 | } 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | options: { 84 | canvas: { 85 | height: 192, 86 | width: 64 87 | } 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /test/specs/options.spec.js: -------------------------------------------------------------------------------- 1 | import plugin from 'chartjs-plugin-datalabels'; 2 | 3 | var OPTIONS = { 4 | align: {auto: true}, 5 | anchor: {auto: true}, 6 | clamp: {auto: true}, 7 | clip: {auto: true}, 8 | display: {auto: true}, 9 | opacity: {auto: true}, 10 | textStrokeColor: {auto: false}, 11 | textStrokeWidth: {auto: false}, 12 | textShadowBlur: {auto: false}, 13 | textShadowColor: {auto: false}, 14 | }; 15 | 16 | describe('options', function() { 17 | jasmine.chart.register(plugin); 18 | 19 | describe('options (scriptable)', function() { 20 | Object.keys(OPTIONS).forEach(function(key) { 21 | it(key + ' should be called with a valid context', function() { 22 | var options = {}; 23 | options[key] = function() {}; 24 | spyOn(options, key); 25 | 26 | var chart = jasmine.chart.acquire({ 27 | type: 'line', 28 | data: { 29 | labels: [0, 1], 30 | datasets: [{ 31 | data: [42, 51] 32 | }, { 33 | data: [2] 34 | }] 35 | }, 36 | options: { 37 | plugins: { 38 | datalabels: options 39 | } 40 | } 41 | }); 42 | 43 | expect(options[key].calls.count()).toBe(3); 44 | 45 | [ 46 | {dataIndex: 0, datasetIndex: 0}, 47 | {dataIndex: 1, datasetIndex: 0}, 48 | {dataIndex: 0, datasetIndex: 1} 49 | ].forEach(function(e, i) { 50 | expect(options[key].calls.argsFor(i)[0]).toEqual({ 51 | active: false, 52 | chart: chart, 53 | dataIndex: e.dataIndex, 54 | dataset: chart.data.datasets[e.datasetIndex], 55 | datasetIndex: e.datasetIndex 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | Object.keys(OPTIONS).forEach(function(key) { 63 | if (OPTIONS[key].auto) { 64 | describe(`options.${key}`, function() { 65 | describe('auto', jasmine.fixture.specs(`options.${key}`)); 66 | }); 67 | } 68 | }); 69 | 70 | describe('options.labels', function() { 71 | describe('auto', jasmine.fixture.specs('options.labels')); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /types/test/options.scriptable.ts: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import {Context} from '../context'; 3 | import {Options} from '../options'; 4 | 5 | const options: Options = { 6 | align: (ctx: Context) => 'end', 7 | anchor: (ctx: Context) => 'end', 8 | backgroundColor: (ctx: Context) => 'blue', 9 | borderColor: (ctx: Context) => 'blue', 10 | borderRadius: (ctx: Context) => 42, 11 | borderWidth: (ctx: Context) => 42, 12 | clamp: (ctx: Context) => false, 13 | clip: (ctx: Context) => false, 14 | color: (ctx: Context) => 'blue', 15 | display: (ctx: Context) => true, 16 | font: (ctx: Context) => ({size: 42}), 17 | labels: { 18 | foo: {}, 19 | bar: null, 20 | bla: { 21 | align: (ctx: Context) => 'end', 22 | anchor: (ctx: Context) => 'end', 23 | backgroundColor: (ctx: Context) => 'blue', 24 | borderColor: (ctx: Context) => 'blue', 25 | borderRadius: (ctx: Context) => 42, 26 | borderWidth: (ctx: Context) => 42, 27 | clamp: (ctx: Context) => false, 28 | clip: (ctx: Context) => false, 29 | color: (ctx: Context) => 'blue', 30 | display: (ctx: Context) => true, 31 | font: (ctx: Context) => ({size: 42}), 32 | offset: (ctx: Context) => 42, 33 | opacity: (ctx: Context) => 0.42, 34 | padding: (ctx: Context) => 42, 35 | rotation: (ctx: Context) => 42, 36 | textAlign: (ctx: Context) => 'end', 37 | textStrokeColor: (ctx: Context) => 'blue', 38 | textStrokeWidth: (ctx: Context) => 42, 39 | textShadowBlur: (ctx: Context) => 42, 40 | textShadowColor: (ctx: Context) => 'blue' 41 | } 42 | }, 43 | offset: (ctx: Context) => 42, 44 | opacity: (ctx: Context) => 0.42, 45 | padding: (ctx: Context) => 42, 46 | rotation: (ctx: Context) => 42, 47 | textAlign: (ctx: Context) => 'end', 48 | textStrokeColor: (ctx: Context) => 'blue', 49 | textStrokeWidth: (ctx: Context) => 42, 50 | textShadowBlur: (ctx: Context) => 42, 51 | textShadowColor: (ctx: Context) => 'blue', 52 | }; 53 | 54 | const chart = new Chart('id', { 55 | type: 'bar', 56 | data: { 57 | labels: [], 58 | datasets: [ 59 | { 60 | data: [], 61 | datalabels: options 62 | } 63 | ] 64 | }, 65 | options: { 66 | plugins: { 67 | datalabels: options 68 | } 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /docs/samples/advanced/custom-labels.md: -------------------------------------------------------------------------------- 1 | # Custom Labels 2 | 3 | Displays the data labels instead of the data values, using a [custom formatter](../../guide/formatting.md#custom-labels). 4 | 5 | ```js chart-editor 6 | // 7 | var labels = [ 8 | 'Mercury', 9 | 'Venus', 10 | 'Earth', 11 | 'Mars', 12 | 'Jupiter', 13 | 'Saturn', 14 | 'Uranus', 15 | 'Neptune', 16 | ]; 17 | 18 | var DATA_COUNT = labels.length; 19 | 20 | Utils.srand(0); 21 | // 22 | 23 | var config = /* */ { 24 | type: 'bar', 25 | data: { 26 | labels: labels, 27 | datasets: [{ 28 | backgroundColor: Utils.colors(0), 29 | data: Utils.numbers({ 30 | count: DATA_COUNT, 31 | min: 0, 32 | max: 100 33 | }) 34 | }] 35 | }, 36 | options: { 37 | plugins: { 38 | datalabels: { 39 | align: 'end', 40 | anchor: 'end', 41 | color: function(context) { 42 | return context.dataset.backgroundColor; 43 | }, 44 | font: function(context) { 45 | var w = context.chart.width; 46 | return { 47 | size: w < 512 ? 12 : 14, 48 | weight: 'bold', 49 | }; 50 | }, 51 | formatter: function(value, context) { 52 | return context.chart.data.labels[context.dataIndex]; 53 | } 54 | } 55 | }, 56 | 57 | // Core options 58 | aspectRatio: 5 / 3, 59 | layout: { 60 | padding: { 61 | top: 32 62 | } 63 | }, 64 | elements: { 65 | line: { 66 | fill: false, 67 | tension: 0.4 68 | } 69 | }, 70 | scales: { 71 | x: { 72 | display: false, 73 | offset: true 74 | }, 75 | y: { 76 | beginAtZero: true 77 | } 78 | } 79 | } 80 | } /* */; 81 | 82 | var actions = [ 83 | { 84 | name: 'Randomize', 85 | handler: function(chart) { 86 | chart.data.datasets.forEach(function(dataset, i) { 87 | dataset.data = dataset.data.map(function(value) { 88 | return Utils.rand(0, 100); 89 | }); 90 | }); 91 | 92 | chart.update(); 93 | } 94 | } 95 | ]; 96 | 97 | module.exports = { 98 | actions: actions, 99 | config: config, 100 | }; 101 | ``` 102 | -------------------------------------------------------------------------------- /docs/guide/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | This plugin provides TypeScript type declaration files bundled in the npm package so that types of this plugin options are checked when building your TypeScript project. 4 | 5 | ## Option Context 6 | 7 | The declaration of the [option context](options.md#option-context) is exported as `Context`: 8 | 9 | ```ts 10 | import {Context} from 'chartjs-plugin-datalabels'; 11 | 12 | const chart = new Chart('foo', { 13 | options: { 14 | plugins: { 15 | datalabels: { 16 | rotation: (ctx: Context) => { 17 | return ctx.dataIndex % 2 ? 180 : 0; 18 | }, 19 | } 20 | } 21 | } 22 | }); 23 | ``` 24 | 25 | Extending this context can be done using one of the following methods: 26 | 27 | ### Custom Interface 28 | 29 | ```ts 30 | import {Context} from 'chartjs-plugin-datalabels'; 31 | 32 | interface FooContext extends Context { 33 | foo?: number; 34 | } 35 | 36 | const chart = new Chart('foo', { 37 | options: { 38 | plugins: { 39 | datalabels: { 40 | rotation: (ctx: FooContext) => ctx.foo || 0, 41 | listeners: { 42 | click: (ctx: FooContext) => { 43 | ctx.foo += (ctx.foo || 0) + 10; 44 | return true; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }); 51 | ``` 52 | 53 | ::: tip 54 | This method allows to declare different context interfaces to use in different charts. 55 | ::: 56 | 57 | ### Module Augmentation 58 | 59 | ```ts 60 | // shims-chartjs-plugin-datalabels.d.ts 61 | import {Context} from 'chartjs-plugin-datalabels'; 62 | 63 | declare module 'chartjs-plugin-datalabels' { 64 | interface Context { 65 | foo?: number; 66 | } 67 | } 68 | ``` 69 | 70 | ```ts 71 | // index.ts 72 | const chart = new Chart('foo', { 73 | options: { 74 | plugins: { 75 | datalabels: { 76 | rotation: (ctx: Context) => ctx.foo || 0, 77 | listeners: { 78 | click: (ctx: Context) => { 79 | ctx.foo += (ctx.foo || 0) + 10; 80 | return true; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | }); 87 | ``` 88 | ::: warning 89 | The augmented `Context` declaration will be the same for all charts. This method should be considered only if all charts should share the same context declaration. Read more about [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation). 90 | ::: 91 | -------------------------------------------------------------------------------- /test/specs/drawing.spec.js: -------------------------------------------------------------------------------- 1 | import plugin from 'chartjs-plugin-datalabels'; 2 | 3 | describe('drawing', function() { 4 | jasmine.chart.register(plugin); 5 | 6 | describe('auto', jasmine.fixture.specs('drawing')); 7 | 8 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/30 9 | it('should not create labels for skipped element', function() { 10 | var chart = jasmine.chart.acquire({ 11 | type: 'line', 12 | data: { 13 | labels: [0, 1, 2, 3, 4], 14 | datasets: [{ 15 | data: [42, null, NaN, undefined, 'foobar'] 16 | }] 17 | } 18 | }); 19 | 20 | var ds0 = chart.getDatasetMeta(0); 21 | 22 | expect(ds0.data[0].skip).toBeFalsy(); 23 | expect(ds0.data[0].$datalabels.length).toBeGreaterThan(0); 24 | 25 | for (var i = 1; i <= 4; ++i) { 26 | expect(ds0.data[i].skip).toBeTruthy(); 27 | expect(ds0.data[i].$datalabels).toEqual([]); 28 | } 29 | }); 30 | 31 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/51 32 | it ('should not create labels for hidden dataset', function() { 33 | var chart = jasmine.chart.acquire({ 34 | type: 'line', 35 | data: { 36 | labels: [0, 1, 2, 3, 4], 37 | datasets: [{ 38 | data: [42, 43, 44, 45], 39 | hidden: true 40 | }] 41 | } 42 | }); 43 | 44 | var ds0 = chart.getDatasetMeta(0); 45 | 46 | expect(chart.isDatasetVisible(0)).toBeFalsy(); 47 | 48 | for (var i = 0; i <= 3; ++i) { 49 | expect(ds0.data[i].$datalabels).toEqual([]); 50 | } 51 | }); 52 | 53 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/51 54 | it ('should destroy labels when dataset become hidden', function() { 55 | var chart = jasmine.chart.acquire({ 56 | type: 'line', 57 | data: { 58 | labels: [0, 1, 2, 3, 4], 59 | datasets: [{ 60 | data: [42, 43, 44, 45] 61 | }] 62 | } 63 | }); 64 | 65 | var ds0 = chart.getDatasetMeta(0); 66 | var i; 67 | 68 | expect(chart.isDatasetVisible(0)).toBeTruthy(); 69 | 70 | for (i = 0; i <= 3; ++i) { 71 | expect(ds0.data[i].$datalabels.length).toBeGreaterThan(0); 72 | } 73 | 74 | chart.data.datasets[0].hidden = true; 75 | chart.update(); 76 | 77 | expect(chart.isDatasetVisible(0)).toBeFalsy(); 78 | 79 | for (i = 0; i <= 3; ++i) { 80 | expect(ds0.data[i].$datalabels).toEqual([]); 81 | } 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chartjs-plugin-datalabels", 3 | "homepage": "https://chartjs-plugin-datalabels.netlify.app", 4 | "description": "Chart.js plugin to display labels on data elements", 5 | "version": "2.2.0", 6 | "license": "MIT", 7 | "main": "dist/chartjs-plugin-datalabels.js", 8 | "module": "dist/chartjs-plugin-datalabels.esm.js", 9 | "types": "types/index.d.ts", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/chartjs/chartjs-plugin-datalabels.git" 13 | }, 14 | "keywords": [ 15 | "chart.js", 16 | "plugin", 17 | "label" 18 | ], 19 | "files": [ 20 | "bower.json", 21 | "dist/*.js", 22 | "types/*.d.ts" 23 | ], 24 | "scripts": { 25 | "bower": "node scripts/create-bower-json", 26 | "build": "rollup -c", 27 | "build:dev": "rollup -c --watch", 28 | "docs": "npm run build && vuepress build docs --no-cache", 29 | "docs:dev": "npm run build && vuepress dev docs --no-cache", 30 | "lint": "eslint . --ext .js,.ts --cache", 31 | "package": "npm run build && node scripts/create-packages.js", 32 | "test-unit": "karma start --single-run --coverage --grep", 33 | "test-unit:dev": "karma start --auto-watch --grep", 34 | "test-types": "tsc -p types/test", 35 | "test": "npm run test-types && npm run test-unit", 36 | "test:dev": "npm run test-unit:dev" 37 | }, 38 | "devDependencies": { 39 | "@simonbrunel/vuepress-plugin-versions": "^0.2.0", 40 | "@typescript-eslint/eslint-plugin": "^5.30.6", 41 | "@typescript-eslint/parser": "^5.30.6", 42 | "@vuepress/plugin-google-analytics": "^1.9.7", 43 | "archiver": "^5.3.1", 44 | "chart.js": "^4.0.0", 45 | "chartjs-test-utils": "^0.4.0", 46 | "eslint": "^8.19.0", 47 | "eslint-config-chartjs": "^0.3.0", 48 | "eslint-plugin-es": "^4.1.0", 49 | "jasmine-core": "^3.99.1", 50 | "karma": "^6.4.0", 51 | "karma-coverage": "^2.2.0", 52 | "karma-firefox-launcher": "^2.1.2", 53 | "karma-jasmine": "^4.0.2", 54 | "karma-jasmine-html-reporter": "^1.7.0", 55 | "karma-rollup-preprocessor": "^7.0.8", 56 | "karma-spec-reporter": "^0.0.34", 57 | "rollup": "^2.77.0", 58 | "rollup-plugin-commonjs": "^10.1.0", 59 | "rollup-plugin-istanbul": "^3.0.0", 60 | "rollup-plugin-node-resolve": "^5.2.0", 61 | "rollup-plugin-terser": "^7.0.2", 62 | "typescript": "^4.7.4", 63 | "vuepress": "~1.8.3", 64 | "vuepress-plugin-redirect": "^1.2.5", 65 | "vuepress-theme-chartjs": "^0.2.0", 66 | "yargs": "^17.5.1" 67 | }, 68 | "peerDependencies": { 69 | "chart.js": ">=3.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import {acquireChart, addMatchers, releaseChart, releaseCharts, specsFromFixtures, triggerMouseEvent} from 'chartjs-test-utils'; 3 | 4 | // force ratio=1 for tests on high-res/retina devices 5 | window.devicePixelRatio = 1; 6 | 7 | jasmine.chart = { 8 | acquire: acquireChart, 9 | release: releaseChart, 10 | 11 | // Since version 1.x, this plugin isn't anymore automatically registered on first 12 | // import. This helper allows to register the given plugin for all specs contained 13 | // in the describe() block from where this method is called. Note that some tests 14 | // require the plugin to **not** be registered. 15 | register: function(plugin) { 16 | if (Chart.registry.plugins.get(plugin.id)) { 17 | throw new Error(`Plugin #${plugin.id} is already registered`); 18 | } 19 | 20 | beforeEach(() => { 21 | Chart.register(plugin); 22 | if (!Chart.registry.plugins.get(plugin.id)) { 23 | throw new Error(`Failed to register plugin #${plugin.id}`); 24 | } 25 | }); 26 | afterEach(() => { 27 | Chart.unregister(plugin); 28 | if (Chart.registry.plugins.get(plugin.id)) { 29 | throw new Error(`Failed to unregister plugin #${plugin.id}`); 30 | } 31 | }); 32 | } 33 | }; 34 | 35 | jasmine.fixture = { 36 | specs: specsFromFixtures 37 | }; 38 | jasmine.triggerMouseEvent = triggerMouseEvent; 39 | 40 | beforeAll(() => { 41 | // Disable colors plugin for tests. 42 | window.Chart.defaults.plugins.colors.enabled = false; 43 | }); 44 | 45 | beforeEach(function() { 46 | addMatchers(); 47 | 48 | Chart.defaults.set({ 49 | animation: false, 50 | responsive: false, 51 | elements: { 52 | arc: { 53 | backgroundColor: 'transparent', 54 | borderColor: 'rgba(0, 0, 0, 0.1)', 55 | borderWidth: 1 56 | }, 57 | point: { 58 | backgroundColor: 'transparent', 59 | borderColor: 'rgba(0, 0, 0, 0.1)', 60 | borderWidth: 1 61 | }, 62 | rectangle: { 63 | backgroundColor: 'transparent', 64 | borderColor: 'rgba(0, 0, 0, 0.1)', 65 | borderWidth: 1 66 | } 67 | }, 68 | plugins: { 69 | legend: { 70 | display: false 71 | }, 72 | title: { 73 | display: false 74 | }, 75 | tooltip: { 76 | display: false 77 | } 78 | } 79 | }); 80 | 81 | Chart.defaults.set('scale', { 82 | display: false, 83 | ticks: { 84 | beginAtZero: true 85 | } 86 | }); 87 | }); 88 | 89 | afterEach(function() { 90 | releaseCharts(); 91 | }); 92 | -------------------------------------------------------------------------------- /docs/guide/migration.md: -------------------------------------------------------------------------------- 1 | # Migration 2 | 3 | ## Migrating to v1.0.0 4 | 5 | ### Breaking Changes 6 | 7 | #### Explicit Plugin Registration 8 | 9 | As described in the [getting started](getting-started.md#integration), it's now required to manually register this plugin, either globally: 10 | 11 | ```js 12 | Chart.plugins.register(ChartDataLabels); 13 | ``` 14 | 15 | or locally: 16 | 17 | ```js 18 | new Chart('foo', { 19 | plugins: [ChartDataLabels] 20 | }) 21 | ``` 22 | 23 | See [chartjs-plugin-datalabels#42](https://github.com/chartjs/chartjs-plugin-datalabels/issues/42) for rationale behind this change. 24 | 25 | #### Extending the Option Context 26 | 27 | In order to extend the [option context](options.md#option-context), you now need to use one of the methods described [in this section](typescript.md#option-context). Peviously, this feature relied on the use of `any`. If for whatever reasons you need that flexibility, the old behavior can be achieved using: 28 | 29 | ```ts 30 | import {Context} from 'chartjs-plugin-datalabels'; 31 | 32 | // OLD BEHAVIOR: NOT RECOMMENDED! 33 | declare module 'chartjs-plugin-datalabels' { 34 | interface Context { 35 | [key: string]: any; 36 | } 37 | } 38 | ``` 39 | 40 | ## Migrating to v2.0.0 41 | 42 | ### Breaking Changes 43 | 44 | Make sure to also read the [Chart.js v3 migration guide](https://www.chartjs.org/docs/latest/getting-started/v3-migration.html) since you may be impacted by more general breaking changes due to this new Chart.js version. 45 | 46 | #### Plugin registration 47 | 48 | Chart.js v3 changed the way to register plugins and now requires to use `Chart.register(plugin)` instead of `Chart.plugins.register(plugin)`. 49 | 50 | ```js 51 | import {Chart} from 'chart.js'; 52 | import ChartDataLabels from 'chartjs-plugin-datalabels'; 53 | 54 | Chart.register(ChartDataLabels); 55 | ``` 56 | 57 | See [Getting Started > Registration](getting-started.html#registration) for details. 58 | 59 | #### Default options 60 | 61 | The plugin default options are now accessible in `Chart.defaults.plugins.datalabels.*` instead of `Chart.defaults.global.plugins.datalabels.*` and can be modified using: 62 | 63 | ```js 64 | Chart.defaults.set('plugins.datalabels', { 65 | color: 'blue', 66 | // ... 67 | }) 68 | ``` 69 | 70 | See [Getting Started > Configuration](getting-started.html#configuration) for details. 71 | 72 | ### Notes 73 | 74 | #### Chart.js type declaration 75 | 76 | Chart.js v3 now provides TypeScript type declaration files bundled in the npm package so it's **not** anymore required to install the `@types/chart.js` package. 77 | -------------------------------------------------------------------------------- /docs/samples/utils.js: -------------------------------------------------------------------------------- 1 | import {color as Color} from 'chart.js/helpers'; 2 | 3 | function fallback(/* values ... */) { 4 | var ilen = arguments.length; 5 | var i = 0; 6 | var v; 7 | 8 | for (; i < ilen; ++i) { 9 | v = arguments[i]; 10 | if (v !== undefined) { 11 | return v; 12 | } 13 | } 14 | } 15 | 16 | export var COLORS = [ 17 | '#FF3784', 18 | '#36A2EB', 19 | '#4BC0C0', 20 | '#F77825', 21 | '#9966FF', 22 | '#00A8C6', 23 | '#379F7A', 24 | '#CC2738', 25 | '#8B628A', 26 | '#8FBE00', 27 | '#606060', 28 | ]; 29 | 30 | // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ 31 | var _seed = Date.now(); 32 | 33 | export function srand(seed) { 34 | _seed = seed; 35 | } 36 | 37 | export function rand(min, max) { 38 | min = min === undefined ? 0 : min; 39 | max = max === undefined ? 1 : max; 40 | _seed = (_seed * 9301 + 49297) % 233280; 41 | return min + (_seed / 233280) * (max - min); 42 | } 43 | 44 | export function numbers(config) { 45 | var cfg = config || {}; 46 | var min = fallback(cfg.min, 0); 47 | var max = fallback(cfg.max, 1); 48 | var from = fallback(cfg.from, []); 49 | var count = fallback(cfg.count, 8); 50 | var decimals = fallback(cfg.decimals, 8); 51 | var continuity = fallback(cfg.continuity, 1); 52 | var dfactor = Math.pow(10, decimals) || 0; 53 | var data = []; 54 | var i, value; 55 | 56 | for (i = 0; i < count; ++i) { 57 | value = (from[i] || 0) + this.rand(min, max); 58 | if (this.rand() <= continuity) { 59 | data.push(Math.round(dfactor * value) / dfactor); 60 | } else { 61 | data.push(null); 62 | } 63 | } 64 | 65 | return data; 66 | } 67 | 68 | export function color(offset) { 69 | var count = COLORS.length; 70 | var index = offset === undefined ? ~~rand(0, count) : offset; 71 | return COLORS[index % count]; 72 | } 73 | 74 | export function colors(config) { 75 | var cfg = config || {}; 76 | var c = cfg.color || color(0); 77 | var count = cfg.count !== undefined ? cfg.count : 8; 78 | var method = cfg.mode ? Color(color)[cfg.mode] : null; 79 | var values = []; 80 | var i, f, v; 81 | 82 | for (i = 0; i < count; ++i) { 83 | f = i / count; 84 | 85 | if (method) { 86 | v = method.call(Color(c), f).rgbString(); 87 | } else { 88 | v = color(i); 89 | } 90 | 91 | values.push(v); 92 | } 93 | 94 | return values; 95 | } 96 | 97 | export function transparentize(c, opacity) { 98 | var alpha = opacity === undefined ? 0.5 : 1 - opacity; 99 | return Color(c).alpha(alpha).rgbString(); 100 | } 101 | -------------------------------------------------------------------------------- /types/test/options.basic.ts: -------------------------------------------------------------------------------- 1 | import {Chart, ChartEvent} from 'chart.js'; 2 | import {Context} from '../context'; 3 | import {Options} from '../options'; 4 | 5 | const options: Options = { 6 | align: 'start', 7 | anchor: 'start', 8 | backgroundColor: 'blue', 9 | borderColor: 'blue', 10 | borderRadius: 42, 11 | borderWidth: 42, 12 | clamp: false, 13 | clip: false, 14 | color: 'blue', 15 | display: true, 16 | font: { 17 | family: 'foo', 18 | lineHeight: 1.42, 19 | size: 42, 20 | style: 'italic', 21 | weight: 200 22 | }, 23 | formatter: (v: string, ctx: Context) => ctx.active ? '' + v : null, 24 | labels: { 25 | foo: {}, 26 | bar: null, 27 | bla: { 28 | align: 'end', 29 | anchor: 'end', 30 | backgroundColor: 'blue', 31 | borderColor: 'blue', 32 | borderRadius: 42, 33 | borderWidth: 42, 34 | clamp: false, 35 | clip: false, 36 | color: 'blue', 37 | display: true, 38 | font: { 39 | family: 'foo', 40 | lineHeight: 1.42, 41 | size: 42, 42 | style: 'italic', 43 | weight: 200 44 | }, 45 | offset: 42, 46 | opacity: 0.42, 47 | listeners: { 48 | click: (ctx: Context, event: ChartEvent) => true, 49 | enter: (ctx: Context, event: ChartEvent) => false, 50 | leave: (ctx: Context, event: ChartEvent) => { 51 | // return void 52 | } 53 | }, 54 | padding: { 55 | top: 42, 56 | right: 42, 57 | bottom: 42, 58 | left: 42 59 | }, 60 | rotation: 42, 61 | textAlign: 'end', 62 | textStrokeColor: 'blue', 63 | textStrokeWidth: 42, 64 | textShadowBlur: 42, 65 | textShadowColor: 'blue' 66 | } 67 | }, 68 | listeners: { 69 | click: (ctx: Context) => true, 70 | enter: (ctx: Context) => false, 71 | leave: (ctx: Context) => { 72 | // return void 73 | } 74 | }, 75 | offset: 42, 76 | opacity: 0.42, 77 | padding: { 78 | top: 42, 79 | right: 42, 80 | bottom: 42, 81 | left: 42 82 | }, 83 | rotation: 42, 84 | textAlign: 'start', 85 | textStrokeColor: 'blue', 86 | textStrokeWidth: 42, 87 | textShadowBlur: 42, 88 | textShadowColor: 'blue' 89 | }; 90 | 91 | const chart = new Chart('id', { 92 | type: 'bar', 93 | data: { 94 | labels: [], 95 | datasets: [ 96 | { 97 | data: [], 98 | datalabels: options 99 | } 100 | ] 101 | }, 102 | options: { 103 | plugins: { 104 | datalabels: options 105 | } 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-fallback-no-width-height.js: -------------------------------------------------------------------------------- 1 | const Chart = window.Chart; 2 | 3 | class TestController extends Chart.DatasetController { 4 | update(mode) { 5 | const {data: elements} = this._cachedMeta; 6 | this.updateElements(elements, 0, elements.length, mode); 7 | } 8 | 9 | updateElements(elements, start, count, mode) { 10 | const reset = mode === 'reset'; 11 | const {xScale, yScale} = this._cachedMeta; 12 | for (let i = start; i < start + count; i++) { 13 | const parsed = !reset && this.getParsed(i); 14 | const x = reset ? xScale.getBasePixel() : xScale.getPixelForValue(parsed.x); 15 | const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed.y); 16 | this.updateElement(elements[i], i, {x, y}, mode); 17 | } 18 | } 19 | 20 | draw() { 21 | for (const element of this._cachedMeta.data) { 22 | element.draw(this._ctx); 23 | } 24 | } 25 | } 26 | TestController.id = 'test'; 27 | TestController.defaults = { 28 | dataElementType: 'testElement' 29 | }; 30 | 31 | class TestElement extends Chart.Element { 32 | draw(ctx) { 33 | ctx.save(); 34 | ctx.strokeStyle = 'blue'; 35 | ctx.strokeRect(this.x, this.y, 64, 32); 36 | ctx.restore(); 37 | } 38 | } 39 | TestElement.id = 'testElement'; 40 | 41 | Chart.register(TestController, TestElement); 42 | 43 | var datasets = []; 44 | 45 | var data = [ 46 | {x: 1, y: 1}, 47 | {x: 1, y: 2}, 48 | {x: 2, y: 1}, 49 | {x: 2, y: 2}, 50 | ]; 51 | 52 | ['start', 'center', 'end'].forEach(function(anchor) { 53 | datasets.push({ 54 | data: data, 55 | datalabels: { 56 | anchor: anchor 57 | }, 58 | width: 128, 59 | height: 96 60 | }); 61 | }); 62 | 63 | export default { 64 | config: { 65 | type: 'test', 66 | data: { 67 | datasets: datasets 68 | }, 69 | options: { 70 | scales: { 71 | x: { 72 | type: 'linear', 73 | min: 0.7, 74 | max: 2.8 75 | }, 76 | y: { 77 | type: 'linear', 78 | min: 0, 79 | max: 2.4 80 | } 81 | }, 82 | plugins: { 83 | datalabels: { 84 | backgroundColor: '#00ff77', 85 | borderColor: 'black', 86 | borderWidth: 2, 87 | font: { 88 | size: 0 89 | }, 90 | padding: 8 91 | } 92 | } 93 | } 94 | }, 95 | options: { 96 | canvas: { 97 | width: 256, 98 | height: 128 99 | }, 100 | run() { 101 | Chart.unregister(TestController, TestElement); 102 | } 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /test/fixtures/options.anchor/anchor-presets-fallback-width-height.js: -------------------------------------------------------------------------------- 1 | const Chart = window.Chart; 2 | 3 | class TestController extends Chart.DatasetController { 4 | update(mode) { 5 | const {data: elements} = this._cachedMeta; 6 | this.updateElements(elements, 0, elements.length, mode); 7 | } 8 | 9 | updateElements(elements, start, count, mode) { 10 | const reset = mode === 'reset'; 11 | const {xScale, yScale} = this._cachedMeta; 12 | for (let i = start; i < start + count; i++) { 13 | const parsed = !reset && this.getParsed(i); 14 | const x = reset ? xScale.getBasePixel() : xScale.getPixelForValue(parsed.x); 15 | const y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed.y); 16 | this.updateElement(elements[i], i, {x, y, width: 64, height: 32}, mode); 17 | } 18 | } 19 | 20 | draw() { 21 | for (const element of this._cachedMeta.data) { 22 | element.draw(this._ctx); 23 | } 24 | } 25 | } 26 | TestController.id = 'test'; 27 | TestController.defaults = { 28 | dataElementType: 'testElement' 29 | }; 30 | 31 | class TestElement extends Chart.Element { 32 | draw(ctx) { 33 | ctx.save(); 34 | ctx.strokeStyle = 'blue'; 35 | ctx.strokeRect(this.x, this.y, this.width, this.height); 36 | ctx.restore(); 37 | } 38 | } 39 | TestElement.id = 'testElement'; 40 | 41 | Chart.register(TestController, TestElement); 42 | 43 | var datasets = []; 44 | 45 | var data = [ 46 | {x: 1, y: 1}, 47 | {x: 1, y: 2}, 48 | {x: 2, y: 1}, 49 | {x: 2, y: 2}, 50 | ]; 51 | 52 | ['start', 'center', 'end'].forEach(function(anchor) { 53 | datasets.push({ 54 | data: data, 55 | datalabels: { 56 | anchor: anchor 57 | }, 58 | width: 128, 59 | height: 96 60 | }); 61 | }); 62 | 63 | export default { 64 | config: { 65 | type: 'test', 66 | data: { 67 | datasets: datasets 68 | }, 69 | options: { 70 | scales: { 71 | x: { 72 | type: 'linear', 73 | min: 0.7, 74 | max: 2.8 75 | }, 76 | y: { 77 | type: 'linear', 78 | min: 0, 79 | max: 2.4 80 | } 81 | }, 82 | plugins: { 83 | datalabels: { 84 | backgroundColor: '#00ff77', 85 | borderColor: 'black', 86 | borderWidth: 2, 87 | font: { 88 | size: 0 89 | }, 90 | padding: 8 91 | } 92 | } 93 | } 94 | }, 95 | options: { 96 | canvas: { 97 | width: 256, 98 | height: 128 99 | }, 100 | run() { 101 | Chart.unregister(TestController, TestElement); 102 | } 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Downloads 7 | Builds 8 | Coverage 9 | Awesome 10 |

11 | 12 | ## Overview 13 | 14 | Highly customizable [Chart.js](https://www.chartjs.org/) plugin that displays labels on data for any type of charts. 15 | 16 | Requires [Chart.js](https://github.com/chartjs/Chart.js/releases) **3.x** or higher. 17 | 18 | ## Documentation 19 | 20 | - [Introduction](https://chartjs-plugin-datalabels.netlify.app/guide/) 21 | - [Getting Started](https://chartjs-plugin-datalabels.netlify.app/guide/getting-started.html) 22 | - [Options](https://chartjs-plugin-datalabels.netlify.app/guide/options.html) 23 | - [Labels](https://chartjs-plugin-datalabels.netlify.app/guide/labels.html) 24 | - [Positioning](https://chartjs-plugin-datalabels.netlify.app/guide/positioning.html) 25 | - [Formatting](https://chartjs-plugin-datalabels.netlify.app/guide/formatting.html) 26 | - [Events](https://chartjs-plugin-datalabels.netlify.app/guide/events.html) 27 | - [TypeScript](https://chartjs-plugin-datalabels.netlify.app/guide/typescript.html) 28 | - [Migration](https://chartjs-plugin-datalabels.netlify.app/guide/migration.html) 29 | - [Samples](https://chartjs-plugin-datalabels.netlify.app/samples/) 30 | 31 | ## Development 32 | 33 | You first need to install node dependencies (requires [Node.js](https://nodejs.org/)): 34 | 35 | ``` 36 | > npm install 37 | ``` 38 | 39 | The following commands will then be available from the repository root: 40 | 41 | ``` 42 | > npm run build // build dist files 43 | > npm run build:dev // build and watch for changes 44 | > npm run test // run all tests and generate code coverage 45 | > npm run test:dev // run all tests and watch for changes 46 | > npm run lint // perform code linting 47 | > npm run lint -- --fix // automatically fix linting problems 48 | > npm run docs // generate documentation (`dist/docs`) 49 | > npm run docs:dev // generate documentation and watch for changes 50 | ``` 51 | 52 | ## License 53 | 54 | `chartjs-plugin-datalabels` is available under the [MIT license](LICENSE.md). 55 | -------------------------------------------------------------------------------- /docs/samples/events/listeners.md: -------------------------------------------------------------------------------- 1 | # Listeners 2 | 3 | Listen and log all [label events](../../guide/events.md) (see the `Output` tab). 4 | 5 | ```js chart-editor 6 | // 7 | var DATA_COUNT = 6; 8 | var labels = []; 9 | 10 | Utils.srand(8); 11 | 12 | for (var idx = 0; idx < DATA_COUNT; ++idx) { 13 | labels.push('' + idx); 14 | } 15 | 16 | function log(type, context) { 17 | var i = context.datasetIndex; 18 | var j = context.dataIndex; 19 | var v = context.dataset.data[j]; 20 | 21 | console.log(type + ': ' + i + '-' + j + ' (' + v + ')') 22 | } 23 | // 24 | 25 | var config = /* */ { 26 | type: 'line', 27 | data: { 28 | labels: labels, 29 | datasets: [{ 30 | backgroundColor: Utils.color(0), 31 | borderColor: Utils.color(0), 32 | data: Utils.numbers({ 33 | count: DATA_COUNT, 34 | min: 0, 35 | max: 100 36 | }), 37 | datalabels: { 38 | align: 'start' 39 | } 40 | }, { 41 | backgroundColor: Utils.color(1), 42 | borderColor: Utils.color(1), 43 | data: Utils.numbers({ 44 | count: DATA_COUNT, 45 | min: 0, 46 | max: 100 47 | }) 48 | }, { 49 | backgroundColor: Utils.color(2), 50 | borderColor: Utils.color(2), 51 | data: Utils.numbers({ 52 | count: DATA_COUNT, 53 | min: 0, 54 | max: 100 55 | }), 56 | datalabels: { 57 | align: 'end' 58 | } 59 | }] 60 | }, 61 | options: { 62 | plugins: { 63 | datalabels: { 64 | backgroundColor: function(context) { 65 | return context.dataset.backgroundColor; 66 | }, 67 | color: 'white', 68 | font: { 69 | weight: 'bold' 70 | }, 71 | offset: 8, 72 | padding: 6, 73 | formatter: Math.round, 74 | listeners: { 75 | enter: function(context) { 76 | log('enter', context); 77 | }, 78 | leave: function(context) { 79 | log('leave', context); 80 | }, 81 | click: function(context) { 82 | log('click', context); 83 | } 84 | } 85 | } 86 | }, 87 | 88 | // Core options 89 | aspectRatio: 5 / 3, 90 | layout: { 91 | padding: { 92 | top: 40, 93 | right: 16, 94 | bottom: 16, 95 | left: 0 96 | } 97 | }, 98 | elements: { 99 | line: { 100 | fill: false, 101 | tension: 0.4 102 | } 103 | }, 104 | scales: { 105 | y: { 106 | stacked: true 107 | } 108 | } 109 | } 110 | } /* */; 111 | 112 | module.exports = { 113 | config: config, 114 | output: 'Interact with labels to log events', 115 | }; 116 | ``` 117 | -------------------------------------------------------------------------------- /types/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const INDENT = { 2 | flatTernaryExpressions: true 3 | }; 4 | const QUOTES = { 5 | avoidEscape: true 6 | }; 7 | 8 | module.exports = { 9 | parser: '@typescript-eslint/parser', 10 | plugins: [ 11 | '@typescript-eslint', 12 | ], 13 | extends: [ 14 | 'plugin:@typescript-eslint/recommended', 15 | ], 16 | rules: { 17 | // Allow our tests to not have to manipulate arguments (e.g. ctx). 18 | '@typescript-eslint/no-unused-vars': 'off', 19 | 20 | // Additional rules not part of @typescript-eslint/recommended. 21 | '@typescript-eslint/array-type': 'error', 22 | '@typescript-eslint/consistent-indexed-object-style': 'error', 23 | '@typescript-eslint/member-delimiter-style': 'error', 24 | '@typescript-eslint/method-signature-style': 'error', 25 | '@typescript-eslint/no-confusing-non-null-assertion': 'error', 26 | '@typescript-eslint/no-dynamic-delete': 'error', 27 | '@typescript-eslint/no-extraneous-class': 'error', 28 | '@typescript-eslint/no-implicit-any-catch': 'error', 29 | '@typescript-eslint/no-parameter-properties': 'error', 30 | '@typescript-eslint/no-require-imports': 'error', 31 | '@typescript-eslint/prefer-for-of': 'error', 32 | '@typescript-eslint/prefer-function-type': 'error', 33 | '@typescript-eslint/prefer-optional-chain': 'error', 34 | '@typescript-eslint/prefer-ts-expect-error': 'error', 35 | '@typescript-eslint/type-annotation-spacing': 'error', 36 | '@typescript-eslint/unified-signatures': 'error', 37 | 38 | // Extension rules (based on eslint-config-chartjs). 39 | '@typescript-eslint/brace-style': ['error', '1tbs'], 'brace-style': 'off', 40 | '@typescript-eslint/comma-dangle': ['error', 'only-multiline'], 'comma-dangle': 'off', 41 | '@typescript-eslint/comma-spacing': 'error', 'comma-spacing': 'off', 42 | '@typescript-eslint/indent': ['error', 2, INDENT], indent: 'off', 43 | '@typescript-eslint/keyword-spacing': 'error', 'keyword-spacing': 'off', 44 | '@typescript-eslint/no-extra-parens': ['error', 'functions'], 'no-extra-parens': 'off', 45 | '@typescript-eslint/no-extra-semi': 'error', 'no-extra-semi': 'off', 46 | '@typescript-eslint/no-loop-func': 'error', 'no-loop-func': 'off', 47 | '@typescript-eslint/no-redeclare': 'error', 'no-redeclare': 'off', 48 | '@typescript-eslint/no-shadow': 'error', 'no-shadow': 'off', 49 | '@typescript-eslint/no-unused-expressions': 'error', 'no-unused-expressions': 'off', 50 | '@typescript-eslint/no-use-before-define': 'error', 'no-use-before-define': 'off', 51 | '@typescript-eslint/quotes': ['error', 'single', QUOTES], quotes: 'off', 52 | '@typescript-eslint/semi': ['error', 'always'], semi: 'off', 53 | '@typescript-eslint/space-before-function-paren': ['error', 'never'], 'space-before-function-paren': 'off', 54 | '@typescript-eslint/space-infix-ops': 'error', 'space-infix-ops': 'off', 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /docs/samples/charts/polar.md: -------------------------------------------------------------------------------- 1 | # Polar Area 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 10; 6 | var labels = []; 7 | 8 | Utils.srand(16); 9 | 10 | for (var i = 0; i < DATA_COUNT; ++i) { 11 | labels.push('' + i); 12 | } 13 | // 14 | 15 | var config = /* */ { 16 | type: 'polarArea', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | backgroundColor: Utils.color(0), 21 | data: Utils.numbers({ 22 | count: DATA_COUNT, 23 | min: 0, 24 | max: 100 25 | }) 26 | }, { 27 | backgroundColor: Utils.color(1), 28 | data: Utils.numbers({ 29 | count: DATA_COUNT, 30 | min: 0, 31 | max: 100 32 | }) 33 | }, { 34 | backgroundColor: Utils.color(2), 35 | data: Utils.numbers({ 36 | count: DATA_COUNT, 37 | min: 0, 38 | max: 100 39 | }) 40 | }] 41 | }, 42 | options: { 43 | plugins: { 44 | datalabels: { 45 | anchor: 'end', 46 | backgroundColor: function(context) { 47 | return context.dataset.backgroundColor; 48 | }, 49 | borderColor: 'white', 50 | borderRadius: 25, 51 | borderWidth: 2, 52 | color: 'white', 53 | font: { 54 | weight: 'bold' 55 | }, 56 | formatter: Math.round, 57 | padding: 6 58 | } 59 | }, 60 | 61 | // Core options 62 | aspectRatio: 4 / 3, 63 | layout: { 64 | padding: 16 65 | }, 66 | elements: { 67 | line: { 68 | fill: false 69 | }, 70 | point: { 71 | hoverRadius: 7, 72 | radius: 5 73 | } 74 | }, 75 | } 76 | } /* */; 77 | 78 | var actions = [ 79 | { 80 | name: 'Randomize', 81 | handler: function(chart) { 82 | chart.data.datasets.forEach(function(dataset, i) { 83 | dataset.backgroundColor = Utils.color(); 84 | dataset.data = dataset.data.map(function(value) { 85 | return Utils.rand(0, 100); 86 | }); 87 | }); 88 | 89 | chart.update(); 90 | } 91 | }, 92 | { 93 | name: 'Add data', 94 | handler: function(chart) { 95 | chart.data.labels.push(chart.data.labels.length); 96 | chart.data.datasets.forEach(function(dataset, i) { 97 | dataset.data.push(Utils.rand(0, 100)); 98 | }); 99 | 100 | chart.update(); 101 | } 102 | }, 103 | { 104 | name: 'Remove data', 105 | handler: function(chart) { 106 | chart.data.labels.shift(); 107 | chart.data.datasets.forEach(function(dataset, i) { 108 | dataset.data.shift(); 109 | }); 110 | 111 | chart.update(); 112 | } 113 | } 114 | ]; 115 | 116 | module.exports = { 117 | actions: actions, 118 | config: config, 119 | }; 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/samples/charts/radar.md: -------------------------------------------------------------------------------- 1 | # Radar 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 6; 6 | var labels = []; 7 | 8 | Utils.srand(6); 9 | 10 | for (var i = 0; i < DATA_COUNT; ++i) { 11 | labels.push('' + i); 12 | } 13 | // 14 | 15 | var config = /* */ { 16 | type: 'radar', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | backgroundColor: Utils.transparentize(Utils.color(0), 0.75), 21 | borderColor: Utils.color(0), 22 | data: Utils.numbers({ 23 | count: DATA_COUNT, 24 | min: 0, 25 | max: 100 26 | }) 27 | }, { 28 | backgroundColor: Utils.transparentize(Utils.color(1), 0.75), 29 | borderColor: Utils.color(1), 30 | data: Utils.numbers({ 31 | count: DATA_COUNT, 32 | min: 0, 33 | max: 100 34 | }) 35 | }, { 36 | backgroundColor: Utils.transparentize(Utils.color(2), 0.75), 37 | borderColor: Utils.color(2), 38 | data: Utils.numbers({ 39 | count: DATA_COUNT, 40 | min: 0, 41 | max: 100 42 | }) 43 | }] 44 | }, 45 | options: { 46 | plugins: { 47 | datalabels: { 48 | backgroundColor: function(context) { 49 | return context.dataset.borderColor; 50 | }, 51 | color: 'white', 52 | font: { 53 | weight: 'bold' 54 | }, 55 | formatter: Math.round, 56 | padding: 8 57 | } 58 | }, 59 | 60 | // Core options 61 | aspectRatio: 4 / 3, 62 | elements: { 63 | point: { 64 | hoverRadius: 7, 65 | radius: 5 66 | } 67 | }, 68 | } 69 | } /* */; 70 | 71 | var actions = [ 72 | { 73 | name: 'Randomize', 74 | handler: function(chart) { 75 | chart.data.datasets.forEach(function(dataset) { 76 | var color = Utils.color(); 77 | dataset.backgroundColor = Utils.transparentize(color, 0.75); 78 | dataset.borderColor = color; 79 | dataset.data = dataset.data.map(function(value) { 80 | return Utils.rand(0, 100); 81 | }); 82 | }); 83 | 84 | chart.update(); 85 | } 86 | }, 87 | { 88 | name: 'Add data', 89 | handler: function(chart) { 90 | chart.data.labels.push(chart.data.labels.length); 91 | chart.data.datasets.forEach(function(dataset) { 92 | dataset.data.push(Utils.rand(0, 100)); 93 | }); 94 | 95 | chart.update(); 96 | } 97 | }, 98 | { 99 | name: 'Remove data', 100 | handler: function(chart) { 101 | if (chart.data.labels.length > 3) { 102 | chart.data.labels.shift(); 103 | chart.data.datasets.forEach(function(dataset) { 104 | dataset.data.shift(); 105 | }); 106 | 107 | chart.update(); 108 | } 109 | } 110 | } 111 | ]; 112 | 113 | module.exports = { 114 | actions: actions, 115 | config: config, 116 | }; 117 | ``` 118 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import {isNullOrUndef} from 'chart.js/helpers'; 2 | 3 | var devicePixelRatio = (function() { 4 | if (typeof window !== 'undefined') { 5 | if (window.devicePixelRatio) { 6 | return window.devicePixelRatio; 7 | } 8 | 9 | // devicePixelRatio is undefined on IE10 10 | // https://stackoverflow.com/a/20204180/8837887 11 | // https://github.com/chartjs/chartjs-plugin-datalabels/issues/85 12 | var screen = window.screen; 13 | if (screen) { 14 | return (screen.deviceXDPI || 1) / (screen.logicalXDPI || 1); 15 | } 16 | } 17 | 18 | return 1; 19 | }()); 20 | 21 | var utils = { 22 | // @todo move this in Chart.helpers.toTextLines 23 | toTextLines: function(inputs) { 24 | var lines = []; 25 | var input; 26 | 27 | inputs = [].concat(inputs); 28 | while (inputs.length) { 29 | input = inputs.pop(); 30 | if (typeof input === 'string') { 31 | lines.unshift.apply(lines, input.split('\n')); 32 | } else if (Array.isArray(input)) { 33 | inputs.push.apply(inputs, input); 34 | } else if (!isNullOrUndef(inputs)) { 35 | lines.unshift('' + input); 36 | } 37 | } 38 | 39 | return lines; 40 | }, 41 | 42 | // @todo move this in Chart.helpers.canvas.textSize 43 | // @todo cache calls of measureText if font doesn't change?! 44 | textSize: function(ctx, lines, font) { 45 | var items = [].concat(lines); 46 | var ilen = items.length; 47 | var prev = ctx.font; 48 | var width = 0; 49 | var i; 50 | 51 | ctx.font = font.string; 52 | 53 | for (i = 0; i < ilen; ++i) { 54 | width = Math.max(ctx.measureText(items[i]).width, width); 55 | } 56 | 57 | ctx.font = prev; 58 | 59 | return { 60 | height: ilen * font.lineHeight, 61 | width: width 62 | }; 63 | }, 64 | 65 | /** 66 | * Returns value bounded by min and max. This is equivalent to max(min, min(value, max)). 67 | * @todo move this method in Chart.helpers.bound 68 | * https://doc.qt.io/qt-5/qtglobal.html#qBound 69 | */ 70 | bound: function(min, value, max) { 71 | return Math.max(min, Math.min(value, max)); 72 | }, 73 | 74 | /** 75 | * Returns an array of pair [value, state] where state is: 76 | * * -1: value is only in a0 (removed) 77 | * * 1: value is only in a1 (added) 78 | */ 79 | arrayDiff: function(a0, a1) { 80 | var prev = a0.slice(); 81 | var updates = []; 82 | var i, j, ilen, v; 83 | 84 | for (i = 0, ilen = a1.length; i < ilen; ++i) { 85 | v = a1[i]; 86 | j = prev.indexOf(v); 87 | 88 | if (j === -1) { 89 | updates.push([v, 1]); 90 | } else { 91 | prev.splice(j, 1); 92 | } 93 | } 94 | 95 | for (i = 0, ilen = prev.length; i < ilen; ++i) { 96 | updates.push([prev[i], -1]); 97 | } 98 | 99 | return updates; 100 | }, 101 | 102 | /** 103 | * https://github.com/chartjs/chartjs-plugin-datalabels/issues/70 104 | */ 105 | rasterize: function(v) { 106 | return Math.round(v * devicePixelRatio) / devicePixelRatio; 107 | } 108 | }; 109 | 110 | export default utils; 111 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const commonjs = require('rollup-plugin-commonjs'); 2 | const istanbul = require('rollup-plugin-istanbul'); 3 | const resolve = require('rollup-plugin-node-resolve'); 4 | const builds = require('./rollup.config'); 5 | const yargs = require('yargs'); 6 | 7 | module.exports = function(karma) { 8 | const args = yargs.argv; 9 | const regex = args.autoWatch ? /s\.js$/ : /s\.min\.js$/; 10 | const pattern = !args.grep || args.grep === true ? '' : args.grep; 11 | const specs = `test/specs/**/*${pattern}*spec.js`; 12 | const output = builds[0].output.filter((v) => v.file.match(regex))[0]; 13 | const build = Object.assign({}, builds[0], {output: output}); 14 | 15 | if (args.autoWatch) { 16 | build.output.sourcemap = 'inline'; 17 | } 18 | 19 | karma.set({ 20 | browsers: ['firefox'], 21 | frameworks: ['jasmine'], 22 | reporters: ['spec', 'kjhtml'], 23 | logLevel: karma.LOG_WARN, 24 | 25 | files: [ 26 | {pattern: './test/fixtures/**/*.js', included: false}, 27 | {pattern: './test/fixtures/**/*.png', included: false}, 28 | 'node_modules/chart.js/dist/chart.umd.js', 29 | 'test/index.js', 30 | 'src/plugin.js', 31 | specs 32 | ], 33 | 34 | // Explicitly disable hardware acceleration to make image 35 | // diff more stable when ran on Travis and dev machine. 36 | // https://github.com/chartjs/Chart.js/pull/5629 37 | customLaunchers: { 38 | firefox: { 39 | base: 'Firefox', 40 | prefs: { 41 | 'layers.acceleration.disabled': true 42 | } 43 | } 44 | }, 45 | 46 | preprocessors: { 47 | 'test/fixtures/**/*.js': ['fixtures'], 48 | 'test/specs/**/*.js': ['rollup'], 49 | 'test/index.js': ['rollup'], 50 | 'src/plugin.js': ['sources'] 51 | }, 52 | 53 | rollupPreprocessor: { 54 | plugins: [ 55 | resolve(), 56 | commonjs() 57 | ], 58 | external: [ 59 | 'chart.js', 60 | 'chartjs-plugin-datalabels', 61 | ], 62 | output: { 63 | format: 'umd', 64 | globals: { 65 | 'chart.js': 'Chart', 66 | 'chartjs-plugin-datalabels': 'ChartDataLabels', 67 | } 68 | } 69 | }, 70 | 71 | customPreprocessors: { 72 | fixtures: { 73 | base: 'rollup', 74 | options: { 75 | output: { 76 | format: 'iife', 77 | name: 'fixture' 78 | } 79 | } 80 | }, 81 | sources: { 82 | base: 'rollup', 83 | options: build 84 | } 85 | } 86 | }); 87 | 88 | if (args.coverage) { 89 | karma.reporters.push('coverage'); 90 | karma.coverageReporter = { 91 | dir: 'coverage/', 92 | reporters: [ 93 | {type: 'html', subdir: 'html'}, 94 | {type: 'lcovonly', subdir: '.'} 95 | ] 96 | }; 97 | [ 98 | karma.rollupPreprocessor, 99 | karma.customPreprocessors.sources.options 100 | ].forEach((v) => { 101 | (v.plugins || (v.plugins = [])).push( 102 | istanbul({ 103 | include: 'src/**/*.js' 104 | })); 105 | }); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /docs/samples/charts/line.md: -------------------------------------------------------------------------------- 1 | # Line 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 8; 6 | var labels = []; 7 | 8 | Utils.srand(8); 9 | 10 | for (var i = 0; i < DATA_COUNT; ++i) { 11 | labels.push('' + i); 12 | } 13 | // 14 | 15 | var config = /* */ { 16 | type: 'line', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | backgroundColor: Utils.color(0), 21 | borderColor: Utils.color(0), 22 | data: Utils.numbers({ 23 | count: DATA_COUNT, 24 | min: 0, 25 | max: 100 26 | }), 27 | datalabels: { 28 | align: 'start', 29 | anchor: 'start' 30 | } 31 | }, { 32 | backgroundColor: Utils.color(1), 33 | borderColor: Utils.color(1), 34 | data: Utils.numbers({ 35 | count: DATA_COUNT, 36 | min: 0, 37 | max: 100 38 | }) 39 | }, { 40 | backgroundColor: Utils.color(2), 41 | borderColor: Utils.color(2), 42 | data: Utils.numbers({ 43 | count: DATA_COUNT, 44 | min: 0, 45 | max: 100 46 | }), 47 | datalabels: { 48 | align: 'end', 49 | anchor: 'end' 50 | } 51 | }] 52 | }, 53 | options: { 54 | plugins: { 55 | datalabels: { 56 | backgroundColor: function(context) { 57 | return context.dataset.backgroundColor; 58 | }, 59 | borderRadius: 4, 60 | color: 'white', 61 | font: { 62 | weight: 'bold' 63 | }, 64 | formatter: Math.round, 65 | padding: 6 66 | } 67 | }, 68 | 69 | // Core options 70 | aspectRatio: 5 / 3, 71 | layout: { 72 | padding: { 73 | top: 32, 74 | right: 16, 75 | bottom: 16, 76 | left: 8 77 | } 78 | }, 79 | elements: { 80 | line: { 81 | fill: false, 82 | tension: 0.4 83 | } 84 | }, 85 | scales: { 86 | y: { 87 | stacked: true 88 | } 89 | } 90 | } 91 | } /* */; 92 | 93 | var actions = [ 94 | { 95 | name: 'Randomize', 96 | handler: function(chart) { 97 | chart.data.datasets.forEach(function(dataset, i) { 98 | dataset.backgroundColor = dataset.borderColor = Utils.color(); 99 | dataset.data = dataset.data.map(function(value) { 100 | return Utils.rand(0, 100); 101 | }); 102 | }); 103 | 104 | chart.update(); 105 | } 106 | }, 107 | { 108 | name: 'Add data', 109 | handler: function(chart) { 110 | chart.data.labels.push(chart.data.labels.length); 111 | chart.data.datasets.forEach(function(dataset, i) { 112 | dataset.data.push(Utils.rand(0, 100)); 113 | }); 114 | 115 | chart.update(); 116 | } 117 | }, 118 | { 119 | name: 'Remove data', 120 | handler: function(chart) { 121 | chart.data.labels.shift(); 122 | chart.data.datasets.forEach(function(dataset, i) { 123 | dataset.data.shift(); 124 | }); 125 | 126 | chart.update(); 127 | } 128 | } 129 | ]; 130 | 131 | module.exports = { 132 | actions: actions, 133 | config: config, 134 | }; 135 | ``` 136 | -------------------------------------------------------------------------------- /docs/samples/charts/bar.md: -------------------------------------------------------------------------------- 1 | # Bar 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 10; 6 | var labels = []; 7 | 8 | Utils.srand(2); 9 | 10 | for (var i = 0; i < DATA_COUNT; ++i) { 11 | labels.push('' + i); 12 | } 13 | // 14 | 15 | var config = /* */ { 16 | type: 'bar', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | backgroundColor: Utils.color(0), 21 | data: Utils.numbers({ 22 | count: DATA_COUNT, 23 | min: 0, 24 | max: 100 25 | }), 26 | datalabels: { 27 | align: 'end', 28 | anchor: 'start' 29 | } 30 | }, { 31 | backgroundColor: Utils.color(1), 32 | data: Utils.numbers({ 33 | count: DATA_COUNT, 34 | min: 0, 35 | max: 100 36 | }), 37 | datalabels: { 38 | align: 'center', 39 | anchor: 'center' 40 | } 41 | }, { 42 | backgroundColor: Utils.color(2), 43 | data: Utils.numbers({ 44 | count: DATA_COUNT, 45 | min: 0, 46 | max: 100 47 | }), 48 | datalabels: { 49 | anchor: 'end', 50 | align: 'start', 51 | } 52 | }] 53 | }, 54 | options: { 55 | plugins: { 56 | datalabels: { 57 | color: 'white', 58 | display: function(context) { 59 | return context.dataset.data[context.dataIndex] > 15; 60 | }, 61 | font: { 62 | weight: 'bold' 63 | }, 64 | formatter: Math.round 65 | } 66 | }, 67 | 68 | // Core options 69 | aspectRatio: 5 / 3, 70 | layout: { 71 | padding: { 72 | top: 24, 73 | right: 16, 74 | bottom: 0, 75 | left: 8 76 | } 77 | }, 78 | elements: { 79 | line: { 80 | fill: false 81 | }, 82 | point: { 83 | hoverRadius: 7, 84 | radius: 5 85 | } 86 | }, 87 | scales: { 88 | x: { 89 | stacked: true 90 | }, 91 | y: { 92 | stacked: true 93 | } 94 | } 95 | } 96 | } /* */; 97 | 98 | var actions = [ 99 | { 100 | name: 'Randomize', 101 | handler: function(chart) { 102 | chart.data.datasets.forEach(function(dataset, i) { 103 | dataset.backgroundColor = Utils.color(); 104 | dataset.data = dataset.data.map(function(value) { 105 | return Utils.rand(0, 100); 106 | }); 107 | }); 108 | 109 | chart.update(); 110 | } 111 | }, 112 | { 113 | name: 'Add data', 114 | handler: function(chart) { 115 | chart.data.labels.push(chart.data.labels.length); 116 | chart.data.datasets.forEach(function(dataset, i) { 117 | dataset.data.push(Utils.rand(0, 100)); 118 | }); 119 | 120 | chart.update(); 121 | } 122 | }, 123 | { 124 | name: 'Remove data', 125 | handler: function(chart) { 126 | chart.data.labels.shift(); 127 | chart.data.datasets.forEach(function(dataset, i) { 128 | dataset.data.shift(); 129 | }); 130 | 131 | chart.update(); 132 | } 133 | } 134 | ]; 135 | 136 | module.exports = { 137 | actions: actions, 138 | config: config, 139 | }; 140 | ``` 141 | -------------------------------------------------------------------------------- /docs/samples/charts/bubble.md: -------------------------------------------------------------------------------- 1 | # Bubble 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 8; 6 | var labels = []; 7 | 8 | Utils.srand(18); 9 | 10 | function generatePoint() { 11 | return { 12 | x: Utils.rand(-100, 100), 13 | y: Utils.rand(-50, 50), 14 | v: Utils.rand(15, 100), 15 | }; 16 | } 17 | 18 | function generateData() { 19 | var data = []; 20 | for (var i = 0; i < DATA_COUNT; ++i) { 21 | data.push(generatePoint()); 22 | } 23 | return data; 24 | } 25 | // 26 | 27 | var config = /* */ { 28 | type: 'bubble', 29 | data: { 30 | datasets: [{ 31 | backgroundColor: Utils.color(0), 32 | borderColor: Utils.color(0), 33 | data: generateData() 34 | }, { 35 | backgroundColor: Utils.color(1), 36 | borderColor: Utils.color(1), 37 | data: generateData() 38 | }] 39 | }, 40 | options: { 41 | plugins: { 42 | datalabels: { 43 | anchor: function(context) { 44 | var value = context.dataset.data[context.dataIndex]; 45 | return value.v < 50 ? 'end' : 'center'; 46 | }, 47 | align: function(context) { 48 | var value = context.dataset.data[context.dataIndex]; 49 | return value.v < 50 ? 'end' : 'center'; 50 | }, 51 | color: function(context) { 52 | var value = context.dataset.data[context.dataIndex]; 53 | return value.v < 50 ? context.dataset.backgroundColor : 'white'; 54 | }, 55 | font: { 56 | weight: 'bold' 57 | }, 58 | formatter: function(value) { 59 | return Math.round(value.v); 60 | }, 61 | offset: 2, 62 | padding: 0 63 | } 64 | }, 65 | 66 | // Core options 67 | aspectRatio: 5 / 3, 68 | layout: { 69 | padding: 16 70 | }, 71 | elements: { 72 | point: { 73 | radius: function(context) { 74 | var value = context.dataset.data[context.dataIndex]; 75 | var size = context.chart.width; 76 | var base = Math.abs(value.v) / 100; 77 | return (size / 24) * base; 78 | } 79 | } 80 | }, 81 | } 82 | } /* */; 83 | 84 | var actions = [ 85 | { 86 | name: 'Randomize', 87 | handler: function(chart) { 88 | chart.data.datasets.forEach(function(dataset, i) { 89 | dataset.backgroundColor = dataset.borderColor = Utils.color(); 90 | dataset.data = generateData(); 91 | }); 92 | 93 | chart.update(); 94 | } 95 | }, 96 | { 97 | name: 'Add data', 98 | handler: function(chart) { 99 | chart.data.labels.push(chart.data.labels.length); 100 | chart.data.datasets.forEach(function(dataset, i) { 101 | dataset.data.push(generatePoint()); 102 | }); 103 | 104 | chart.update(); 105 | } 106 | }, 107 | { 108 | name: 'Remove data', 109 | handler: function(chart) { 110 | chart.data.labels.shift(); 111 | chart.data.datasets.forEach(function(dataset, i) { 112 | dataset.data.shift(); 113 | }); 114 | 115 | chart.update(); 116 | } 117 | } 118 | ]; 119 | 120 | module.exports = { 121 | actions: actions, 122 | config: config, 123 | }; 124 | ``` 125 | -------------------------------------------------------------------------------- /test/specs/utils.spec.js: -------------------------------------------------------------------------------- 1 | import utils from '../../src/utils'; 2 | 3 | describe('utils.js', function() { 4 | describe('toTextLines', function() { 5 | var toTextLines = utils.toTextLines; 6 | 7 | it('should return an array containing the input string', function() { 8 | expect(toTextLines('')).toEqual(['']); 9 | expect(toTextLines('foo')).toEqual(['foo']); 10 | expect(toTextLines('foo bar')).toEqual(['foo bar']); 11 | }); 12 | it('should return an array with converted values', function() { 13 | expect(toTextLines(null)).toEqual(['null']); 14 | expect(toTextLines(undefined)).toEqual(['undefined']); 15 | expect(toTextLines(42)).toEqual(['42']); 16 | expect(toTextLines(true)).toEqual(['true']); 17 | }); 18 | it('should return an array of strings if inputs is an array', function() { 19 | expect(toTextLines([])).toEqual([]); 20 | expect(toTextLines(['foo'])).toEqual(['foo']); 21 | expect(toTextLines(['foo', 'bar'])).toEqual(['foo', 'bar']); 22 | }); 23 | it('should split the input string if it contains \\n', function() { 24 | expect(toTextLines('foo\nbar')).toEqual(['foo', 'bar']); 25 | expect(toTextLines('foo\nbar\nbla')).toEqual(['foo', 'bar', 'bla']); 26 | }); 27 | it('should preserve spaces when splitting strings', function() { 28 | expect(toTextLines('foo \n bar')).toEqual(['foo ', ' bar']); 29 | expect(toTextLines('foo \n bar \n bla')).toEqual(['foo ', ' bar ', ' bla']); 30 | }); 31 | it('should flatten children arrays in the correct order', function() { 32 | expect(toTextLines(['foo', [['bar', 'xxx'], 'bla']])).toEqual(['foo', 'bar', 'xxx', 'bla']); 33 | }); 34 | it('should split strings children in the correct order', function() { 35 | expect(toTextLines(['foo', [['bar\nxxx'], 'bla\nyyy']])).toEqual(['foo', 'bar', 'xxx', 'bla', 'yyy']); 36 | }); 37 | }); 38 | 39 | describe('arrayDiff', function() { 40 | var arrayDiff = utils.arrayDiff; 41 | 42 | it ('should return an empty array if inputs are also empty', function() { 43 | expect(arrayDiff([], [])).toEqual([]); 44 | }); 45 | it ('should return an array of [value, state] with proper state', function() { 46 | var a0 = [42, 51, 22]; 47 | var a1 = [42, 11]; 48 | 49 | expect(arrayDiff(a0, a1)).toEqual([[11, 1], [51, -1], [22, -1]]); 50 | expect(arrayDiff(a1, a0)).toEqual([[51, 1], [22, 1], [11, -1]]); 51 | expect(arrayDiff(a0, [])).toEqual([[42, -1], [51, -1], [22, -1]]); 52 | expect(arrayDiff([], a0)).toEqual([[42, 1], [51, 1], [22, 1]]); 53 | expect(arrayDiff(a0, a0)).toEqual([]); 54 | }); 55 | it ('should not modify input arrays', function() { 56 | var a0 = [42, 51]; 57 | var a1 = [42, 11]; 58 | 59 | arrayDiff(a0, a1); 60 | 61 | expect(a0).toEqual([42, 51]); 62 | expect(a1).toEqual([42, 11]); 63 | }); 64 | it ('should preserve value references', function() { 65 | var o0 = {}; 66 | var o1 = {}; 67 | var o2 = {}; 68 | var a0 = [o0]; 69 | var a1 = [o1, o2]; 70 | var diff = arrayDiff(a0, a1); 71 | 72 | expect(diff).toEqual([[o1, 1], [o2, 1], [o0, -1]]); 73 | expect(diff[0][0]).toBe(o1); 74 | expect(diff[1][0]).toBe(o2); 75 | expect(diff[2][0]).toBe(o0); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/samples/scriptable/mirror.md: -------------------------------------------------------------------------------- 1 | # Mirror 2 | 3 | Use [scriptable options](../../guide/options.md#scriptable-options) to mirror the [*anchor*](../../guide/positioning.md#anchoring), [*align*](../../guide/positioning.md#alignment-and-offset) and 4 | [*rotation*](../../guide/positioning.md#rotation) options around the horizontal scale origin. 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 16; 9 | var labels = []; 10 | 11 | Utils.srand(0); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'bar', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | backgroundColor: Utils.color(0), 24 | borderColor: Utils.color(0), 25 | data: Utils.numbers({ 26 | count: DATA_COUNT, 27 | min: -100, 28 | max: 100 29 | }) 30 | }] 31 | }, 32 | options: { 33 | plugins: { 34 | datalabels: { 35 | align: function(context) { 36 | var value = context.dataset.data[context.dataIndex]; 37 | return value > 0 ? 'end' : 'start'; 38 | }, 39 | anchor: function(context) { 40 | var value = context.dataset.data[context.dataIndex]; 41 | return value > 0 ? 'end' : 'start'; 42 | }, 43 | borderRadius: 4, 44 | color: 'white', 45 | rotation: function(context) { 46 | var value = context.dataset.data[context.dataIndex]; 47 | return value > 0 ? 45 : 180 - 45; 48 | }, 49 | backgroundColor: function(context) { 50 | return context.dataset.backgroundColor; 51 | }, 52 | formatter: Math.round, 53 | padding: 6, 54 | } 55 | }, 56 | 57 | // Core options 58 | aspectRatio: 5 / 3, 59 | layout: { 60 | padding: { 61 | top: 32, 62 | right: 24, 63 | bottom: 24, 64 | left: 0 65 | } 66 | }, 67 | elements: { 68 | line: { 69 | fill: false, 70 | tension: 0.4 71 | }, 72 | point: { 73 | hoverRadius: 7, 74 | radius: 5 75 | } 76 | }, 77 | }, 78 | } // 79 | 80 | var actions = [ 81 | { 82 | name: 'Randomize', 83 | handler: function(chart) { 84 | chart.data.datasets.forEach(function(dataset, i) { 85 | dataset.backgroundColor = dataset.borderColor = Utils.color(); 86 | dataset.data = dataset.data.map(function(value) { 87 | return Utils.rand(-100, 100); 88 | }); 89 | }); 90 | 91 | chart.update(); 92 | } 93 | }, 94 | { 95 | name: 'Add data', 96 | handler: function(chart) { 97 | chart.data.labels.push(chart.data.labels.length); 98 | chart.data.datasets.forEach(function(dataset, i) { 99 | dataset.data.push(Utils.rand(-100, 100)); 100 | }); 101 | 102 | chart.update(); 103 | } 104 | }, 105 | { 106 | name: 'Remove data', 107 | handler: function(chart) { 108 | chart.data.labels.shift(); 109 | chart.data.datasets.forEach(function(dataset, i) { 110 | dataset.data.shift(); 111 | }); 112 | 113 | chart.update(); 114 | } 115 | } 116 | ] 117 | 118 | module.exports = { 119 | actions: actions, 120 | config: config, 121 | }; 122 | ``` 123 | -------------------------------------------------------------------------------- /docs/samples/scriptable/indices.md: -------------------------------------------------------------------------------- 1 | # Indices 2 | 3 | Use [scriptable options](../../guide/options.md#scriptable-options) to alternate the style of the 4 | labels based on the data indices. 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 16; 9 | var labels = []; 10 | 11 | Utils.srand(4); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'line', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | backgroundColor: Utils.color(0), 24 | borderColor: Utils.color(0), 25 | data: Utils.numbers({ 26 | count: DATA_COUNT, 27 | min: 0, 28 | max: 100 29 | }) 30 | }] 31 | }, 32 | options: { 33 | plugins: { 34 | datalabels: { 35 | align: function(context) { 36 | return context.dataIndex % 2 ? 'end' : 'center'; 37 | }, 38 | backgroundColor: function(context) { 39 | return context.dataIndex % 2 ? 40 | context.dataset.borderColor : 41 | 'rgba(255, 255, 255, 0.8)'; 42 | }, 43 | borderColor: function(context) { 44 | return context.dataIndex % 2 ? null : context.dataset.borderColor; 45 | }, 46 | borderWidth: function(context) { 47 | return context.dataIndex % 2 ? 0 : 2; 48 | }, 49 | color: function(context) { 50 | return context.dataIndex % 2 ? 'white' : context.dataset.borderColor; 51 | }, 52 | font: { 53 | weight: 'bold', 54 | }, 55 | formatter: function(value, context) { 56 | return context.dataIndex + ': ' + Math.round(value) + '\\''; 57 | }, 58 | offset: 8, 59 | padding: 6, 60 | } 61 | }, 62 | 63 | // Core options 64 | aspectRatio: 5 / 3, 65 | layout: { 66 | padding: { 67 | top: 32, 68 | right: 24, 69 | bottom: 24, 70 | left: 0 71 | } 72 | }, 73 | elements: { 74 | line: { 75 | fill: false, 76 | tension: 0.4 77 | } 78 | }, 79 | } 80 | } /* */; 81 | 82 | var actions = [ 83 | { 84 | name: 'Randomize', 85 | handler: function(chart) { 86 | chart.data.datasets.forEach(function(dataset, i) { 87 | var color = Utils.color(); 88 | dataset.backgroundColor = color; 89 | dataset.borderColor = color; 90 | dataset.data = dataset.data.map(function(value) { 91 | return Utils.rand(0, 100); 92 | }); 93 | }); 94 | 95 | chart.update(); 96 | } 97 | }, 98 | { 99 | name: 'Add data', 100 | handler: function(chart) { 101 | chart.data.labels.push(chart.data.labels.length); 102 | chart.data.datasets.forEach(function(dataset, i) { 103 | dataset.data.push(Utils.rand(0, 100)); 104 | }); 105 | 106 | chart.update(); 107 | } 108 | }, 109 | { 110 | name: 'Remove data', 111 | handler: function(chart) { 112 | chart.data.labels.shift(); 113 | chart.data.datasets.forEach(function(dataset, i) { 114 | dataset.data.shift(); 115 | }); 116 | 117 | chart.update(); 118 | } 119 | } 120 | ]; 121 | 122 | module.exports = { 123 | actions: actions, 124 | config: config, 125 | }; 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/guide/formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting 2 | 3 | ## Data Transformation 4 | 5 | Data values are converted to string (`'' + value`). If `value` is an object, the following rules apply first: 6 | 7 | - `value = value.label` if defined and not null 8 | - else `value = value.r` if defined and not null 9 | - else `value = 'key[0]: value[key[0]], key[1]: value[key[1]], ...'` 10 | 11 | This default behavior can be overridden thanks to the `formatter` option. It accepts a function called for every data and that takes two arguments: 12 | 13 | - `value`: the current data value 14 | - `context`: contextual information (see [option context](options.md#option-context)) 15 | 16 | Example: 17 | 18 | ```javascript 19 | formatter: function(value, context) { 20 | return context.dataIndex + ': ' + Math.round(value*100) + '%'; 21 | } 22 | 23 | // label for data at index 0 with value 0.23: "0: 23%" 24 | // label for data at index 1 with value 0.42: "1: 42%" 25 | // ... 26 | ``` 27 | 28 | ::: tip 29 | The first argument being the value, you can directly use generic methods: 30 | ::: 31 | 32 | ```javascript 33 | formatter: Math.round 34 | formatter: Math.floor 35 | formatter: Math.ceil 36 | // ... 37 | ``` 38 | 39 | ## Custom Labels 40 | 41 | It's also possible to display text other than the data values, for example, the associated labels: 42 | 43 | ```javascript 44 | new Chart('id', { 45 | type: 'bar', 46 | data: { 47 | labels: ['foo', 'bar'], 48 | datasets: [{ 49 | data: [42, 24] 50 | }] 51 | }, 52 | options: { 53 | plugins: { 54 | datalabels: { 55 | formatter: function(value, context) { 56 | return context.chart.data.labels[context.dataIndex]; 57 | } 58 | } 59 | } 60 | } 61 | }); 62 | 63 | // label for data at index 0: "foo" 64 | // label for data at index 1: "bar" 65 | // ... 66 | ``` 67 | 68 | ::: tip 69 | `chart.data.labels` is given as an example but it works with any source: 70 | ::: 71 | 72 | ```javascript 73 | context.dataset.data[context.dataIndex].label; // labels in each data object 74 | context.dataset.labels[context.dataIndex]; // labels store in the dataset 75 | globalLabels[context.dataIndex]; // labels store outside the chart 76 | // ... 77 | ``` 78 | 79 | ## Multiline Labels 80 | 81 | Labels can be displayed on multiple lines by using the newline character (`\n`) between each line or by providing an array of strings where each item represents a new line. 82 | 83 | Example: 84 | 85 | ```javascript 86 | formatter: function(value) { 87 | return 'line1\nline2\n' + value; 88 | // eq. return ['line1', 'line2', value] 89 | } 90 | ``` 91 | 92 | ::: tip 93 | The space between each line can be adjusted using the `font.lineHeight` option. 94 | ::: 95 | 96 | ## Text Alignment 97 | 98 | The `textAlign` option only applies to [multiline labels](#multiline-labels) and specifies the text alignment being used when drawing the label text (see [`CanvasRenderingContext2D.textAlign`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign)). Note that right-to-left text detection based on the current locale is not currently implemented. 99 | 100 | Supported values for `textAlign`: 101 | 102 | - `'start'` (default): the text is left-aligned 103 | - `'center'`: the text is centered 104 | - `'end'`: the text is right-aligned 105 | - `'left'`: alias of `'start'` 106 | - `'right'`: alias of `'end'` 107 | -------------------------------------------------------------------------------- /docs/guide/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | This plugin currently supports the following label events: 4 | 5 | | **Name** | **Chart events1** | **Description** 6 | | ---- | ---- | ---- 7 | | `enter` | `mousemove` | the mouse is moved over a label 8 | | `leave` | `mousemove` | the mouse is moved out of a label 9 | | `click` | `click` | the mouse's primary button is pressed and released on a label 10 | 11 | ::: tip 12 | 1 [Chart.js events](https://www.chartjs.org/docs/latest/configuration/interactions.html#events) that need to be enabled in order to get the associated label event working. Note that by default Chart.js enables `"mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"`, meaning that label events work out-of-the-box. 13 | ::: 14 | 15 | ## Listeners 16 | 17 | The `listeners` option allows to register callbacks to be notified when an event is detected on a specific label. This option is an object where each property represents an event, the key being the type of the event to listen and the value being a callback accepting the `context` and `event` arguments. 18 | 19 | The `context` contains the same information as for [scriptable options](options.md#option-context), can be modified (e.g. add new properties) and thus, **if the callback explicitly returns `true`**, the label is updated with the new context and the chart re-rendered. This allows to implement visual interactions with labels such as highlight, selection, etc. 20 | 21 | Listeners can be registered for any label (`options.plugin.datalabels.listener.*`) or for labels of a specific dataset (`dataset.datalabels.listeners.*`). 22 | 23 | ::: tip 24 | If no listener is registered, incoming events are immediately ignored, preventing extra computation such as intersecting label bounding box. That means there should be no performance penalty for configurations that don't use events. 25 | ::: 26 | 27 | ## Example 28 | 29 | ```javascript 30 | { 31 | data: { 32 | datasets: [{ 33 | datalabels: { 34 | listeners: { 35 | click: function(context, event) { 36 | // Receives `click` events only for labels of the first dataset. 37 | // The clicked label index is available in `context.dataIndex`. 38 | console.log('label ' + context.dataIndex + ' has been clicked!'); 39 | console.log('mouse is at position x:', event.x, 'and y:', event.y); 40 | 41 | if (event.native.ctrlKey) { 42 | console.log('control key is pressed!'); 43 | } 44 | } 45 | } 46 | } 47 | }, { 48 | //... 49 | }] 50 | }, 51 | options: { 52 | plugins: { 53 | datalabels: { 54 | listeners: { 55 | enter: function(context, event) { 56 | // Receives `enter` events for any labels of any dataset. Indices of the 57 | // clicked label are: `context.datasetIndex` and `context.dataIndex`. 58 | // For example, we can modify keep track of the hovered state and 59 | // return `true` to update the label and re-render the chart. 60 | context.hovered = true; 61 | return true; 62 | }, 63 | leave: function(context, event) { 64 | // Receives `leave` events for any labels of any dataset. 65 | context.hovered = false; 66 | return true; 67 | } 68 | }, 69 | color: function(context) { 70 | // Change the label text color based on our new `hovered` context value. 71 | return context.hovered ? "blue" : "gray"; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /docs/samples/scriptable/dataset.md: -------------------------------------------------------------------------------- 1 | # Dataset 2 | 3 | Use [scriptable options](../../guide/options.md#scriptable-options) to position labels outside 4 | the filled region between two datasets. 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 12; 9 | var labels = []; 10 | 11 | Utils.srand(26); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'line', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | backgroundColor: Utils.transparentize(Utils.color(0)), 24 | pointBackgroundColor: Utils.color(1), 25 | borderColor: Utils.color(1), 26 | data: Utils.numbers({ 27 | count: DATA_COUNT, 28 | min: -25, 29 | max: 25 30 | }), 31 | fill: '+1', 32 | }, { 33 | pointBackgroundColor: Utils.color(6), 34 | borderColor: Utils.color(6), 35 | data: Utils.numbers({ 36 | count: DATA_COUNT, 37 | min: -100, 38 | max: 100 39 | }) 40 | }] 41 | }, 42 | options: { 43 | plugins: { 44 | datalabels: { 45 | align: function(context) { 46 | var index = context.dataIndex; 47 | var datasets = context.chart.data.datasets; 48 | var v0 = datasets[0].data[index]; 49 | var v1 = datasets[1].data[index]; 50 | var invert = v0 - v1 > 0; 51 | return context.datasetIndex === 0 ? 52 | invert ? 'end' : 'start' : 53 | invert ? 'start' : 'end'; 54 | }, 55 | backgroundColor: function(context) { 56 | return context.dataset.borderColor; 57 | }, 58 | borderRadius: 4, 59 | color: 'white', 60 | font: { 61 | weight: 'bold' 62 | }, 63 | offset: 8, 64 | padding: 6, 65 | formatter: Math.round 66 | } 67 | }, 68 | 69 | // Core options 70 | aspectRatio: 5 / 3, 71 | layout: { 72 | padding: { 73 | top: 32, 74 | right: 24, 75 | bottom: 32, 76 | left: 0 77 | } 78 | }, 79 | elements: { 80 | line: { 81 | borderWidth: 2, 82 | fill: false, 83 | tension: 0.4 84 | } 85 | }, 86 | } 87 | } /* */; 88 | 89 | var actions = [ 90 | { 91 | name: 'Randomize', 92 | handler: function(chart) { 93 | chart.data.datasets.forEach(function(dataset, i) { 94 | var color = Utils.color(); 95 | dataset.borderColor = color; 96 | dataset.pointBackgroundColor = color; 97 | dataset.data = dataset.data.map(function(value) { 98 | return Utils.rand(i % 2 ? -100 : -25, i % 2 ? 100 : 25); 99 | }); 100 | }); 101 | 102 | chart.update(); 103 | } 104 | }, 105 | { 106 | name: 'Add data', 107 | handler: function(chart) { 108 | chart.data.labels.push(chart.data.labels.length); 109 | chart.data.datasets.forEach(function(dataset, i) { 110 | dataset.data.push(Utils.rand(i % 2 ? -100 : -25, i % 2 ? 100 : 25)); 111 | }); 112 | 113 | chart.update(); 114 | } 115 | }, 116 | { 117 | name: 'Remove data', 118 | handler: function(chart) { 119 | chart.data.labels.shift(); 120 | chart.data.datasets.forEach(function(dataset, i) { 121 | dataset.data.shift(); 122 | }); 123 | 124 | chart.update(); 125 | } 126 | } 127 | ]; 128 | 129 | module.exports = { 130 | actions: actions, 131 | config: config, 132 | }; 133 | ``` 134 | -------------------------------------------------------------------------------- /test/specs/defaults.spec.js: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import plugin from 'chartjs-plugin-datalabels'; 3 | 4 | describe('defaults.js', function() { 5 | var expected = { 6 | align: 'center', 7 | anchor: 'center', 8 | backgroundColor: null, 9 | borderColor: null, 10 | borderRadius: 0, 11 | borderWidth: 0, 12 | clamp: false, 13 | clip: false, 14 | color: undefined, 15 | display: true, 16 | font: { 17 | family: undefined, 18 | lineHeight: 1.2, 19 | size: undefined, 20 | style: undefined, 21 | weight: null 22 | }, 23 | labels: undefined, 24 | listeners: {}, 25 | offset: 4, 26 | opacity: 1, 27 | padding: { 28 | top: 4, 29 | right: 4, 30 | bottom: 4, 31 | left: 4 32 | }, 33 | rotation: 0, 34 | textAlign: 'start', 35 | textStrokeColor: undefined, 36 | textStrokeWidth: 0, 37 | textShadowBlur: 0, 38 | textShadowColor: undefined, 39 | // can't test formatter?! 40 | }; 41 | 42 | jasmine.chart.register(plugin); 43 | 44 | it('should be registered as global plugin options', function() { 45 | var globals = Chart.defaults.plugins.datalabels; 46 | expect(globals).toEqual(jasmine.objectContaining(expected)); 47 | }); 48 | it('should be called with default options', function() { 49 | var spy = spyOn(plugin, 'afterDatasetUpdate'); 50 | 51 | var chart = jasmine.chart.acquire({ 52 | type: 'line', 53 | data: { 54 | datasets: [{ 55 | data: [] 56 | }] 57 | } 58 | }); 59 | 60 | expect(spy).toHaveBeenCalled(); 61 | 62 | var args = spy.calls.first().args; 63 | expect(args[0]).toBe(chart); 64 | expect(args[2]).toEqual(jasmine.objectContaining(expected)); 65 | expect(args[2].formatter).toBe(Chart.defaults.plugins.datalabels.formatter); 66 | }); 67 | 68 | describe('default formatter', function() { 69 | var formatter = null; 70 | 71 | beforeEach(() => { 72 | formatter = Chart.defaults.plugins.datalabels.formatter; 73 | }); 74 | 75 | it('should null if value is null or undefined', function() { 76 | expect(formatter()).toBeNull(); 77 | expect(formatter(null)).toBeNull(); 78 | expect(formatter(undefined)).toBeNull(); 79 | }); 80 | it('should return input strings unchanged', function() { 81 | expect(formatter('')).toBe(''); 82 | expect(formatter('foo')).toBe('foo'); 83 | expect(formatter('foo\nbar')).toBe('foo\nbar'); 84 | }); 85 | it('should convert numbers and booleans to strings', function() { 86 | expect(formatter(42)).toBe('42'); 87 | expect(formatter(42.5)).toBe('42.5'); 88 | expect(formatter(true)).toBe('true'); 89 | expect(formatter(false)).toBe('false'); 90 | }); 91 | it('should convert dates to formatted strings', function() { 92 | var now = new Date(); 93 | expect(typeof formatter(now)).toBe('string'); 94 | expect(formatter(now)).toBe(now.toString()); 95 | }); 96 | it('should return value.label if defined', function() { 97 | expect(formatter({label: 42, r: 51})).toBe('42'); 98 | expect(formatter({label: 'foo', r: 51})).toBe('foo'); 99 | }); 100 | it('should return value.r if value.label is undefined', function() { 101 | expect(formatter({r: 42})).toBe('42'); 102 | expect(formatter({r: 'foo'})).toBe('foo'); 103 | }); 104 | it('should return serialized object values if value.label|r are undefined', function() { 105 | expect(formatter({a: 'foo', b: 42, c: true})).toBe('a: foo, b: 42, c: true'); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /docs/guide/positioning.md: -------------------------------------------------------------------------------- 1 | # Positioning 2 | 3 | ## Anchoring 4 | 5 | An anchor point is defined by an orientation vector and a position on the data element. The orientation depends on the scale type (vertical, horizontal or radial). The position is calculated based on the `anchor` option and the orientation vector. 6 | 7 | Supported values for `anchor`: 8 | - `'center'` (default): element center 9 | - `'start'`: lowest element boundary 10 | - `'end'`: highest element boundary 11 | 12 | ![chartjs-plugin-datalabels](../assets/anchor.png) 13 | 14 | ## Clamping 15 | 16 | The `clamp` option, when `true`, enforces the anchor position to be calculated based on the *visible geometry* of the associated element (i.e. part inside the chart area). 17 | 18 | ![chartjs-plugin-datalabels](../assets/clamp.png) 19 | 20 | ::: tip 21 | If the element is fully hidden (i.e. entirely outside the chart area), anchor points will **not** be adjusted and thus will also be outside the viewport. 22 | ::: 23 | 24 | ## Alignment and Offset 25 | 26 | The `align` option defines the position of the label relative to the anchor point position and orientation. Its value can be expressed either by a number representing the clockwise angle (in degree) or by one of the following string presets: 27 | 28 | - `'center'` (default): the label is centered on the anchor point 29 | - `'start'`: the label is positioned before the anchor point, following the same direction 30 | - `'end'`: the label is positioned after the anchor point, following the same direction 31 | - `'right'`: the label is positioned to the right of the anchor point (0°) 32 | - `'bottom'`: the label is positioned to the bottom of the anchor point (90°) 33 | - `'left'`: the label is positioned to the left of the anchor point (180°) 34 | - `'top'`: the label is positioned to the top of the anchor point (270°) 35 | 36 | The `offset` represents the distance (in pixels) to pull the label *away* from the anchor point. This option is **not applicable** when `align` is `'center'`. Also note that if `align` is `'start'`, the label is moved in the opposite direction. The default value is `4`. 37 | 38 | ![chartjs-plugin-datalabels](../assets/align.png) 39 | 40 | ## Rotation 41 | 42 | This option controls the clockwise rotation angle (in degrees) of the label, the rotation center point being the label center. The default value is `0` (no rotation). 43 | 44 | ## Visibility 45 | 46 | The `display` option controls the visibility of labels and accepts the following values: 47 | 48 | - `true` (default): the label is drawn 49 | - `false`: the label is hidden 50 | - `'auto'`: the label is hidden if it [overlap](#overlap) with another label 51 | 52 | This option is [scriptable](options.md#scriptable-options), so it's possible to show/hide specific labels: 53 | 54 | ```javascript 55 | display: function(context) { 56 | return context.dataIndex % 2; // display labels with an odd index 57 | } 58 | ``` 59 | 60 | ## Overlap 61 | 62 | The `display: 'auto'` option can be used to prevent overlapping labels, based on the following rules when two labels overlap: 63 | 64 | - if both labels are `display: true`, they will be drawn overlapping 65 | - if both labels are `display: 'auto'`, the one with the highest data index will be hidden. If labels are at the same data index, the one with the highest dataset index will be hidden 66 | - if one label is `display: true` and the other one is `display: 'auto'`, the one with `'auto'` will be hidden (whatever the data/dataset indices) 67 | 68 | ::: tip 69 | Labels with `display: false` don't contribute to the overlap detection. 70 | ::: 71 | 72 | ## Clipping 73 | 74 | When the `clip` option is `true`, the part of the label which is outside the chart area will be masked (see [CanvasRenderingContext2D.clip()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/clip)) 75 | -------------------------------------------------------------------------------- /docs/samples/charts/doughnut.md: -------------------------------------------------------------------------------- 1 | # Doughnut 2 | 3 | ```js chart-editor 4 | // 5 | var DATA_COUNT = 10; 6 | var labels = []; 7 | 8 | Utils.srand(4); 9 | 10 | for (var i = 0; i < DATA_COUNT; ++i) { 11 | labels.push('' + i); 12 | } 13 | // 14 | 15 | var config = /* */ { 16 | type: 'doughnut', 17 | data: { 18 | labels: labels, 19 | datasets: [{ 20 | backgroundColor: Utils.colors({ 21 | color: Utils.color(0), 22 | count: DATA_COUNT 23 | }), 24 | data: Utils.numbers({ 25 | count: DATA_COUNT, 26 | min: 0, 27 | max: 100 28 | }), 29 | datalabels: { 30 | anchor: 'end' 31 | } 32 | }, { 33 | backgroundColor: Utils.colors({ 34 | color: Utils.color(1), 35 | count: DATA_COUNT 36 | }), 37 | data: Utils.numbers({ 38 | count: DATA_COUNT, 39 | min: 0, 40 | max: 100 41 | }), 42 | datalabels: { 43 | anchor: 'center', 44 | backgroundColor: null, 45 | borderWidth: 0 46 | } 47 | }, { 48 | backgroundColor: Utils.colors({ 49 | color: Utils.color(2), 50 | count: DATA_COUNT 51 | }), 52 | data: Utils.numbers({ 53 | count: DATA_COUNT, 54 | min: 0, 55 | max: 100 56 | }), 57 | datalabels: { 58 | anchor: 'start' 59 | } 60 | }] 61 | }, 62 | options: { 63 | plugins: { 64 | datalabels: { 65 | backgroundColor: function(context) { 66 | return context.dataset.backgroundColor; 67 | }, 68 | borderColor: 'white', 69 | borderRadius: 25, 70 | borderWidth: 2, 71 | color: 'white', 72 | display: function(context) { 73 | var dataset = context.dataset; 74 | var count = dataset.data.length; 75 | var value = dataset.data[context.dataIndex]; 76 | return value > count * 1.5; 77 | }, 78 | font: { 79 | weight: 'bold' 80 | }, 81 | padding: 6, 82 | formatter: Math.round 83 | } 84 | }, 85 | 86 | // Core options 87 | aspectRatio: 4 / 3, 88 | cutoutPercentage: 32, 89 | layout: { 90 | padding: 32 91 | }, 92 | elements: { 93 | line: { 94 | fill: false 95 | }, 96 | point: { 97 | hoverRadius: 7, 98 | radius: 5 99 | } 100 | }, 101 | } 102 | } /* */; 103 | 104 | var actions = [ 105 | { 106 | name: 'Randomize', 107 | handler: function(chart) { 108 | chart.data.datasets.forEach(function(dataset, i) { 109 | dataset.backgroundColor = dataset.data.map(function(value) { 110 | return Utils.color(); 111 | }); 112 | dataset.data = dataset.data.map(function(value) { 113 | return Utils.rand(0, 100); 114 | }); 115 | }); 116 | 117 | chart.update(); 118 | } 119 | }, 120 | { 121 | name: 'Add data', 122 | handler: function(chart) { 123 | chart.data.labels.push(chart.data.labels.length); 124 | chart.data.datasets.forEach(function(dataset, i) { 125 | dataset.backgroundColor.push(Utils.color()); 126 | dataset.data.push(Utils.rand(0, 100)); 127 | }); 128 | 129 | chart.update(); 130 | } 131 | }, 132 | { 133 | name: 'Remove data', 134 | handler: function(chart) { 135 | chart.data.labels.shift(); 136 | chart.data.datasets.forEach(function(dataset, i) { 137 | dataset.backgroundColor.shift(); 138 | dataset.data.shift(); 139 | }); 140 | 141 | chart.update(); 142 | } 143 | } 144 | ]; 145 | 146 | module.exports = { 147 | actions: actions, 148 | config: config, 149 | }; 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/samples/scriptable/data.md: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | Use [scriptable options](../../guide/options.md#scriptable-options) to display increasing values 4 | and decreasing values differently. 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 16; 9 | var labels = []; 10 | 11 | Utils.srand(4); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'line', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | backgroundColor: Utils.color(0), 24 | borderColor: Utils.color(0), 25 | data: Utils.numbers({ 26 | count: DATA_COUNT, 27 | min: -100, 28 | max: 100 29 | }) 30 | }] 31 | }, 32 | options: { 33 | plugins: { 34 | datalabels: { 35 | align: function(context) { 36 | var index = context.dataIndex; 37 | var curr = context.dataset.data[index]; 38 | var prev = context.dataset.data[index - 1]; 39 | var next = context.dataset.data[index + 1]; 40 | return prev < curr && next < curr ? 'end' : 41 | prev > curr && next > curr ? 'start' : 42 | 'center'; 43 | }, 44 | backgroundColor: 'rgba(255, 255, 255, 0.7)', 45 | borderColor: 'rgba(128, 128, 128, 0.7)', 46 | borderRadius: 4, 47 | borderWidth: 1, 48 | color: function(context) { 49 | var i = context.dataIndex; 50 | var value = context.dataset.data[i]; 51 | var prev = context.dataset.data[i - 1]; 52 | var diff = prev !== undefined ? value - prev : 0; 53 | return diff < 0 ? Utils.color(0) : 54 | diff > 0 ? Utils.color(1) : 55 | 'gray'; 56 | }, 57 | font: { 58 | size: 11, 59 | weight: 'bold', 60 | }, 61 | offset: 8, 62 | formatter: function(value, context) { 63 | var i = context.dataIndex; 64 | var prev = context.dataset.data[i - 1]; 65 | var diff = prev !== undefined ? prev - value : 0; 66 | var glyph = diff < 0 ? '\u25B2' : diff > 0 ? '\u25BC' : '\u25C6'; 67 | return glyph + ' ' + Math.round(value); 68 | }, 69 | padding: 6 70 | } 71 | }, 72 | 73 | // Core options 74 | aspectRatio: 5 / 3, 75 | layout: { 76 | padding: { 77 | top: 32, 78 | right: 24, 79 | bottom: 8, 80 | left: 0 81 | } 82 | }, 83 | elements: { 84 | line: { 85 | fill: false, 86 | tension: 0.4 87 | } 88 | }, 89 | } 90 | } /* */; 91 | 92 | var actions = [ 93 | { 94 | name: 'Randomize', 95 | handler: function(chart) { 96 | chart.data.datasets.forEach(function(dataset, i) { 97 | var color = Utils.color(); 98 | dataset.backgroundColor = color; 99 | dataset.borderColor = color; 100 | dataset.data = dataset.data.map(function(value) { 101 | return Utils.rand(-100, 100); 102 | }); 103 | }); 104 | 105 | chart.update(); 106 | } 107 | }, 108 | { 109 | name: 'Add data', 110 | handler: function(chart) { 111 | chart.data.labels.push(chart.data.labels.length); 112 | chart.data.datasets.forEach(function(dataset, i) { 113 | dataset.data.push(Utils.rand(-100, 100)); 114 | }); 115 | 116 | chart.update(); 117 | } 118 | }, 119 | { 120 | name: 'Remove data', 121 | handler: function(chart) { 122 | chart.data.labels.shift(); 123 | chart.data.datasets.forEach(function(dataset, i) { 124 | dataset.data.shift(); 125 | }); 126 | 127 | chart.update(); 128 | } 129 | } 130 | ]; 131 | 132 | module.exports = { 133 | actions: actions, 134 | config: config, 135 | }; 136 | ``` 137 | -------------------------------------------------------------------------------- /docs/samples/events/highlight.md: -------------------------------------------------------------------------------- 1 | # Highlight 2 | 3 | Listen for mouse [`enter` and `leave` events](../../guide/events.md) to modify the label style 4 | using [scriptable options](../../guide/options.md#scriptable-options). 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 8; 9 | var labels = []; 10 | 11 | Utils.srand(0); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'line', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | backgroundColor: Utils.color(0), 24 | borderColor: Utils.color(0), 25 | data: Utils.numbers({ 26 | count: DATA_COUNT, 27 | min: 0, 28 | max: 100 29 | }), 30 | datalabels: { 31 | align: 'start' 32 | } 33 | }, { 34 | backgroundColor: Utils.color(1), 35 | borderColor: Utils.color(1), 36 | data: Utils.numbers({ 37 | count: DATA_COUNT, 38 | min: 0, 39 | max: 100 40 | }) 41 | }, { 42 | backgroundColor: Utils.color(2), 43 | borderColor: Utils.color(2), 44 | data: Utils.numbers({ 45 | count: DATA_COUNT, 46 | min: 0, 47 | max: 100 48 | }), 49 | datalabels: { 50 | align: 'end' 51 | } 52 | }] 53 | }, 54 | options: { 55 | plugins: { 56 | datalabels: { 57 | backgroundColor: function(context) { 58 | return context.hovered ? context.dataset.backgroundColor : 'white'; 59 | }, 60 | borderColor: function(context) { 61 | return context.dataset.backgroundColor; 62 | }, 63 | borderRadius: 16, 64 | borderWidth: 3, 65 | color: function(context) { 66 | return context.hovered ? 'white' : context.dataset.backgroundColor; 67 | }, 68 | font: { 69 | weight: 'bold' 70 | }, 71 | offset: 8, 72 | formatter: Math.round, 73 | listeners: { 74 | enter: function(context) { 75 | context.hovered = true; 76 | return true; 77 | }, 78 | leave: function(context) { 79 | context.hovered = false; 80 | return true; 81 | } 82 | } 83 | } 84 | }, 85 | 86 | // Core options 87 | aspectRatio: 5 / 3, 88 | layout: { 89 | padding: { 90 | top: 42, 91 | right: 16, 92 | bottom: 16, 93 | left: 8 94 | } 95 | }, 96 | elements: { 97 | line: { 98 | fill: false, 99 | tension: 0.4 100 | } 101 | }, 102 | scales: { 103 | y: { 104 | stacked: true 105 | } 106 | } 107 | } 108 | } /* */; 109 | 110 | var actions = [ 111 | { 112 | name: 'Randomize', 113 | handler: function(chart) { 114 | chart.data.datasets.forEach(function(dataset, i) { 115 | dataset.backgroundColor = dataset.borderColor = Utils.color(); 116 | dataset.data = dataset.data.map(function(value) { 117 | return Utils.rand(0, 100); 118 | }); 119 | }); 120 | 121 | chart.update(); 122 | } 123 | }, 124 | { 125 | name: 'Add data', 126 | handler: function(chart) { 127 | chart.data.labels.push(chart.data.labels.length); 128 | chart.data.datasets.forEach(function(dataset, i) { 129 | dataset.data.push(Utils.rand(0, 100)); 130 | }); 131 | 132 | chart.update(); 133 | } 134 | }, 135 | { 136 | name: 'Remove data', 137 | handler: function(chart) { 138 | chart.data.labels.shift(); 139 | chart.data.datasets.forEach(function(dataset, i) { 140 | dataset.data.shift(); 141 | }); 142 | 143 | chart.update(); 144 | } 145 | } 146 | ]; 147 | 148 | module.exports = { 149 | actions: actions, 150 | config: config, 151 | }; 152 | ``` 153 | -------------------------------------------------------------------------------- /src/hitbox.js: -------------------------------------------------------------------------------- 1 | import {merge} from 'chart.js/helpers'; 2 | 3 | var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; // eslint-disable-line es/no-number-minsafeinteger 4 | var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line es/no-number-maxsafeinteger 5 | 6 | function rotated(point, center, angle) { 7 | var cos = Math.cos(angle); 8 | var sin = Math.sin(angle); 9 | var cx = center.x; 10 | var cy = center.y; 11 | 12 | return { 13 | x: cx + cos * (point.x - cx) - sin * (point.y - cy), 14 | y: cy + sin * (point.x - cx) + cos * (point.y - cy) 15 | }; 16 | } 17 | 18 | function projected(points, axis) { 19 | var min = MAX_INTEGER; 20 | var max = MIN_INTEGER; 21 | var origin = axis.origin; 22 | var i, pt, vx, vy, dp; 23 | 24 | for (i = 0; i < points.length; ++i) { 25 | pt = points[i]; 26 | vx = pt.x - origin.x; 27 | vy = pt.y - origin.y; 28 | dp = axis.vx * vx + axis.vy * vy; 29 | min = Math.min(min, dp); 30 | max = Math.max(max, dp); 31 | } 32 | 33 | return { 34 | min: min, 35 | max: max 36 | }; 37 | } 38 | 39 | function toAxis(p0, p1) { 40 | var vx = p1.x - p0.x; 41 | var vy = p1.y - p0.y; 42 | var ln = Math.sqrt(vx * vx + vy * vy); 43 | 44 | return { 45 | vx: (p1.x - p0.x) / ln, 46 | vy: (p1.y - p0.y) / ln, 47 | origin: p0, 48 | ln: ln 49 | }; 50 | } 51 | 52 | var HitBox = function() { 53 | this._rotation = 0; 54 | this._rect = { 55 | x: 0, 56 | y: 0, 57 | w: 0, 58 | h: 0 59 | }; 60 | }; 61 | 62 | merge(HitBox.prototype, { 63 | center: function() { 64 | var r = this._rect; 65 | return { 66 | x: r.x + r.w / 2, 67 | y: r.y + r.h / 2 68 | }; 69 | }, 70 | 71 | update: function(center, rect, rotation) { 72 | this._rotation = rotation; 73 | this._rect = { 74 | x: rect.x + center.x, 75 | y: rect.y + center.y, 76 | w: rect.w, 77 | h: rect.h 78 | }; 79 | }, 80 | 81 | contains: function(point) { 82 | var me = this; 83 | var margin = 1; 84 | var rect = me._rect; 85 | 86 | point = rotated(point, me.center(), -me._rotation); 87 | 88 | return !(point.x < rect.x - margin 89 | || point.y < rect.y - margin 90 | || point.x > rect.x + rect.w + margin * 2 91 | || point.y > rect.y + rect.h + margin * 2); 92 | }, 93 | 94 | // Separating Axis Theorem 95 | // https://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169 96 | intersects: function(other) { 97 | var r0 = this._points(); 98 | var r1 = other._points(); 99 | var axes = [ 100 | toAxis(r0[0], r0[1]), 101 | toAxis(r0[0], r0[3]) 102 | ]; 103 | var i, pr0, pr1; 104 | 105 | if (this._rotation !== other._rotation) { 106 | // Only separate with r1 axis if the rotation is different, 107 | // else it's enough to separate r0 and r1 with r0 axis only! 108 | axes.push( 109 | toAxis(r1[0], r1[1]), 110 | toAxis(r1[0], r1[3]) 111 | ); 112 | } 113 | 114 | for (i = 0; i < axes.length; ++i) { 115 | pr0 = projected(r0, axes[i]); 116 | pr1 = projected(r1, axes[i]); 117 | 118 | if (pr0.max < pr1.min || pr1.max < pr0.min) { 119 | return false; 120 | } 121 | } 122 | 123 | return true; 124 | }, 125 | 126 | /** 127 | * @private 128 | */ 129 | _points: function() { 130 | var me = this; 131 | var rect = me._rect; 132 | var angle = me._rotation; 133 | var center = me.center(); 134 | 135 | return [ 136 | rotated({x: rect.x, y: rect.y}, center, angle), 137 | rotated({x: rect.x + rect.w, y: rect.y}, center, angle), 138 | rotated({x: rect.x + rect.w, y: rect.y + rect.h}, center, angle), 139 | rotated({x: rect.x, y: rect.y + rect.h}, center, angle) 140 | ]; 141 | } 142 | }); 143 | 144 | export default HitBox; 145 | -------------------------------------------------------------------------------- /docs/samples/advanced/multiple-labels.md: -------------------------------------------------------------------------------- 1 | # Multiple Labels 2 | 3 | Use [multiple labels configuration](../../guide/labels.md#multiple-labels) to display 3 labels 4 | per data, one for the `index`, one for the `label` and one for the `value`. Move the mouse over 5 | the chart to display label ids. 6 | 7 | ```js chart-editor 8 | // 9 | var DATA_COUNT = 4; 10 | var labels = [ 11 | 'Earth', 12 | 'Mars', 13 | 'Saturn', 14 | 'Jupiter' 15 | ]; 16 | 17 | Utils.srand(4); 18 | // 19 | 20 | var config = /* */ { 21 | type: 'doughnut', 22 | data: { 23 | labels: labels, 24 | datasets: [{ 25 | backgroundColor: Utils.colors({ 26 | color: Utils.color(1), 27 | mode: 'darken' 28 | }), 29 | hoverBorderColor: 'white', 30 | data: Utils.numbers({ 31 | count: DATA_COUNT, 32 | min: 0, 33 | max: 100 34 | }), 35 | datalabels: { 36 | labels: { 37 | index: { 38 | align: 'end', 39 | anchor: 'end', 40 | color: function(ctx) { 41 | return ctx.dataset.backgroundColor; 42 | }, 43 | font: {size: 18}, 44 | formatter: function(value, ctx) { 45 | return ctx.active 46 | ? 'index' 47 | : '#' + (ctx.dataIndex + 1); 48 | }, 49 | offset: 8, 50 | opacity: function(ctx) { 51 | return ctx.active ? 1 : 0.5; 52 | } 53 | }, 54 | name: { 55 | align: 'top', 56 | font: {size: 16}, 57 | formatter: function(value, ctx) { 58 | return ctx.active 59 | ? 'name' 60 | : ctx.chart.data.labels[ctx.dataIndex]; 61 | } 62 | }, 63 | value: { 64 | align: 'bottom', 65 | backgroundColor: function(ctx) { 66 | var value = ctx.dataset.data[ctx.dataIndex]; 67 | return value > 50 ? 'white' : null; 68 | }, 69 | borderColor: 'white', 70 | borderWidth: 2, 71 | borderRadius: 4, 72 | color: function(ctx) { 73 | var value = ctx.dataset.data[ctx.dataIndex]; 74 | return value > 50 75 | ? ctx.dataset.backgroundColor 76 | : 'white'; 77 | }, 78 | formatter: function(value, ctx) { 79 | return ctx.active 80 | ? 'value' 81 | : Math.round(value * 1000) / 1000; 82 | }, 83 | padding: 4 84 | } 85 | } 86 | } 87 | }] 88 | }, 89 | options: { 90 | plugins: { 91 | datalabels: { 92 | color: 'white', 93 | display: function(ctx) { 94 | return ctx.dataset.data[ctx.dataIndex] > 10; 95 | }, 96 | font: { 97 | weight: 'bold', 98 | }, 99 | offset: 0, 100 | padding: 0 101 | } 102 | }, 103 | 104 | // Core options 105 | aspectRatio: 3 / 2, 106 | cutoutPercentage: 8, 107 | layout: { 108 | padding: 16 109 | }, 110 | elements: { 111 | line: { 112 | fill: false, 113 | tension: 0.4 114 | }, 115 | point: { 116 | hoverRadius: 7, 117 | radius: 5 118 | } 119 | }, 120 | } 121 | } /* */; 122 | 123 | var actions = [ 124 | { 125 | name: 'Randomize', 126 | handler: function(chart) { 127 | chart.data.datasets.forEach(function(dataset, i) { 128 | dataset.backgroundColor = Utils.colors({ 129 | color: Utils.color(), 130 | mode: 'darken' 131 | }); 132 | dataset.data = dataset.data.map(function(value) { 133 | return Utils.rand(0, 100); 134 | }); 135 | }); 136 | chart.update(); 137 | } 138 | } 139 | ]; 140 | 141 | module.exports = { 142 | actions: actions, 143 | config: config, 144 | }; 145 | ``` 146 | -------------------------------------------------------------------------------- /docs/samples/events/selection.md: -------------------------------------------------------------------------------- 1 | # Selection 2 | 3 | [Click events](../../guide/events.md) are handled to select labels, returning `true` to re-render 4 | the chart and update labels (see the `Output` tab). 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 8; 9 | var selection = []; 10 | var labels = []; 11 | 12 | Utils.srand(7); 13 | 14 | for (var idx = 0; idx < DATA_COUNT; ++idx) { 15 | labels.push('' + idx); 16 | } 17 | 18 | function lookup(context) { 19 | var dataset = context.datasetIndex; 20 | var index = context.dataIndex; 21 | var i, ilen; 22 | 23 | for (i = 0, ilen = selection.length; i < ilen; ++i) { 24 | if (selection[i].dataset === dataset && selection[i].index === index) { 25 | return i; 26 | } 27 | } 28 | 29 | return -1; 30 | } 31 | 32 | function isSelected(context) { 33 | return lookup(context) !== -1; 34 | } 35 | 36 | function log(selected) { 37 | console.log('selection: ' + selected.map(function(item) { 38 | return item.value; 39 | }).join(', ')); 40 | } 41 | 42 | function select(context) { 43 | var dataset = context.datasetIndex; 44 | var index = context.dataIndex; 45 | var value = context.dataset.data[index]; 46 | 47 | selection.push({ 48 | dataset: dataset, 49 | index: index, 50 | value: value 51 | }); 52 | 53 | log(selection); 54 | } 55 | 56 | function deselect(context) { 57 | var index = lookup(context); 58 | if (index !== -1) { 59 | selection.splice(index, 1); 60 | log(selection); 61 | } 62 | } 63 | // 64 | 65 | var config = /* */ { 66 | type: 'line', 67 | data: { 68 | labels: labels, 69 | datasets: [{ 70 | backgroundColor: Utils.color(0), 71 | borderColor: Utils.color(0), 72 | data: Utils.numbers({ 73 | count: DATA_COUNT, 74 | decimals: 0, 75 | min: 0, 76 | max: 100 77 | }), 78 | datalabels: { 79 | align: 'start' 80 | } 81 | }, { 82 | backgroundColor: Utils.color(1), 83 | borderColor: Utils.color(1), 84 | data: Utils.numbers({ 85 | count: DATA_COUNT, 86 | decimals: 0, 87 | min: 0, 88 | max: 100 89 | }) 90 | }, { 91 | backgroundColor: Utils.color(2), 92 | borderColor: Utils.color(2), 93 | data: Utils.numbers({ 94 | count: DATA_COUNT, 95 | decimals: 0, 96 | min: 0, 97 | max: 100 98 | }), 99 | datalabels: { 100 | align: 'end' 101 | } 102 | }] 103 | }, 104 | options: { 105 | plugins: { 106 | datalabels: { 107 | backgroundColor: function(context) { 108 | return isSelected(context) 109 | ? context.dataset.backgroundColor 110 | : 'white'; 111 | }, 112 | borderColor: function(context) { 113 | return context.dataset.backgroundColor; 114 | }, 115 | borderWidth: 2, 116 | color: function(context) { 117 | return isSelected(context) 118 | ? 'white' 119 | : context.dataset.backgroundColor; 120 | }, 121 | font: { 122 | weight: 'bold' 123 | }, 124 | offset: 8, 125 | padding: 6, 126 | listeners: { 127 | click: function(context) { 128 | if (isSelected(context)) { 129 | deselect(context); 130 | } else { 131 | select(context); 132 | } 133 | 134 | return true; 135 | } 136 | } 137 | } 138 | }, 139 | 140 | // Core options 141 | aspectRatio: 5 / 3, 142 | layout: { 143 | padding: { 144 | top: 42, 145 | right: 16, 146 | bottom: 32, 147 | left: 8 148 | } 149 | }, 150 | elements: { 151 | line: { 152 | fill: false, 153 | tension: 0.4 154 | } 155 | }, 156 | scales: { 157 | y: { 158 | stacked: true 159 | } 160 | } 161 | } 162 | } /* */; 163 | 164 | module.exports = { 165 | config: config, 166 | output: 'Click on labels to log events' 167 | }; 168 | ``` 169 | -------------------------------------------------------------------------------- /docs/guide/labels.md: -------------------------------------------------------------------------------- 1 | # Labels 2 | 3 | By default, a single label is created per data, however, it's possible to define multiple labels for each data element using the `labels` option. This option is an object where each property represents a new label, the key being the label key and the value being the options specific to each label. These options are merged on top of the options defined at the chart **and** dataset levels. 4 | 5 | ## Multiple Labels 6 | 7 | The following snippet creates two labels for every data element, the first with `title` as key and the second with `value` as key. The `title` label text is blue with font in bold. The `value` label text is green with normal font. 8 | 9 | ```js 10 | { 11 | options: { 12 | plugins: { 13 | datalabels: { 14 | color: 'blue', 15 | labels: { 16 | title: { 17 | font: { 18 | weight: 'bold' 19 | } 20 | }, 21 | value: { 22 | color: 'green' 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ## Dataset Overrides 32 | 33 | While the previous example creates multiple labels with the same options for all datasets, it's possible to add, modify and remove labels for specific datasets by referring to the label key. 34 | 35 | ### Modifying Labels 36 | 37 | To modify a label for a specific dataset, create an entry in the `labels` dataset options using the same key: 38 | 39 | ```js 40 | { 41 | data: { 42 | datasets: [{ 43 | // First dataset. 44 | datalabels: { 45 | color: 'yellow' 46 | } 47 | }, { 48 | // Second dataset. 49 | datalabels: { 50 | labels: { 51 | title: { 52 | color: 'green' 53 | } 54 | } 55 | } 56 | }] 57 | }, 58 | options: { 59 | plugins: { 60 | datalabels: { 61 | color: 'pink', 62 | labels: { 63 | value: {}, 64 | title: { 65 | color: 'blue' 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | This example creates for each data element in the *first* dataset: 75 | - a `value` label with text in yellow 76 | - a `title` label with text in blue 77 | 78 | and for each data element in the *second* dataset: 79 | - a `value` label with text in pink 80 | - a `title` label with text in green 81 | 82 | ::: warning IMPORTANT 83 | Options defined under each `labels.` always override options defined at the chart **and** dataset level (in the previous example, the `title` label text of the *first* dataset is blue, not yellow). 84 | ::: 85 | 86 | ### Adding Label 87 | 88 | To add a new label to a specific dataset, create an entry under the `labels` dataset options using a *inexistent* label key. The following example creates one label (`title`) for each data element in the *first* dataset and two labels (`title` and `value`) for each data element in the *second* dataset: 89 | 90 | ```js 91 | { 92 | data: { 93 | datasets: [{ 94 | // First dataset. 95 | }, { 96 | // Second dataset. 97 | datalabels: { 98 | labels: { 99 | value: { 100 | color: 'green' 101 | } 102 | } 103 | } 104 | }] 105 | }, 106 | options: { 107 | plugins: { 108 | datalabels: { 109 | labels: { 110 | title: { 111 | color: 'blue' 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | ### Removing Label 121 | 122 | To remove a label for a specific dataset, create an `null` entry under the `labels` dataset options for the key of the label to remove. The following example removes the `title` label in the *second* dataset: 123 | 124 | ```js 125 | { 126 | data: { 127 | datasets: [{ 128 | // First dataset. 129 | }, { 130 | // Second dataset. 131 | datalabels: { 132 | labels: { 133 | title: null 134 | } 135 | } 136 | }] 137 | }, 138 | options: { 139 | plugins: { 140 | datalabels: { 141 | labels: { 142 | title: { 143 | color: 'blue' 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/samples/scriptable/interactions.md: -------------------------------------------------------------------------------- 1 | # Interactions 2 | 3 | Use [scriptable options](../../guide/options.md#scriptable-options) to change the style and 4 | content of the labels when the user interacts with the chart. 5 | 6 | ```js chart-editor 7 | // 8 | var DATA_COUNT = 8; 9 | var labels = []; 10 | 11 | Utils.srand(100); 12 | 13 | for (var i = 0; i < DATA_COUNT; ++i) { 14 | labels.push('' + i); 15 | } 16 | // 17 | 18 | var config = /* */ { 19 | type: 'line', 20 | data: { 21 | labels: labels, 22 | datasets: [{ 23 | label: 'France', 24 | backgroundColor: Utils.color(0), 25 | borderColor: Utils.color(0), 26 | data: Utils.numbers({ 27 | count: DATA_COUNT, 28 | min: 10, 29 | max: 100 30 | }), 31 | datalabels: { 32 | align: function(context) { 33 | return context.active ? 'start' : 'center'; 34 | } 35 | } 36 | }, { 37 | label: 'Canada', 38 | backgroundColor: Utils.color(1), 39 | borderColor: Utils.color(1), 40 | data: Utils.numbers({ 41 | count: DATA_COUNT, 42 | min: 0, 43 | max: 100 44 | }) 45 | }, { 46 | label: 'USA', 47 | backgroundColor: Utils.color(2), 48 | borderColor: Utils.color(2), 49 | data: Utils.numbers({ 50 | count: DATA_COUNT, 51 | min: 0, 52 | max: 100 53 | }), 54 | datalabels: { 55 | align: function(context) { 56 | return context.active ? 'end' : 'center'; 57 | } 58 | } 59 | }] 60 | }, 61 | options: { 62 | plugins: { 63 | datalabels: { 64 | backgroundColor: function(context) { 65 | return context.active ? context.dataset.backgroundColor : 'white'; 66 | }, 67 | borderColor: function(context) { 68 | return context.dataset.backgroundColor; 69 | }, 70 | borderRadius: function(context) { 71 | return context.active ? 0 : 32; 72 | }, 73 | borderWidth: 3, 74 | color: function(context) { 75 | return context.active ? 'white' : context.dataset.backgroundColor; 76 | }, 77 | font: { 78 | weight: 'bold' 79 | }, 80 | formatter: function(value, context) { 81 | value = Math.round(value * 100) / 100; 82 | return context.active 83 | ? context.dataset.label + '\\n' + value + '%' 84 | : Math.round(value); 85 | }, 86 | offset: 8, 87 | padding: 5, 88 | textAlign: 'center' 89 | } 90 | }, 91 | 92 | // Core options 93 | aspectRatio: 5 / 3, 94 | layout: { 95 | padding: { 96 | bottom: 16, 97 | right: 40, 98 | left: 8, 99 | top: 40 100 | } 101 | }, 102 | hover: { 103 | mode: 'index', 104 | intersect: false 105 | }, 106 | elements: { 107 | line: { 108 | fill: false, 109 | tension: 0.4 110 | } 111 | }, 112 | scales: { 113 | y: { 114 | stacked: true 115 | } 116 | } 117 | } 118 | } // 119 | 120 | var actions = [ 121 | { 122 | name: 'Randomize', 123 | handler: function(chart) { 124 | chart.data.datasets.forEach(function(dataset, i) { 125 | dataset.backgroundColor = dataset.borderColor = Utils.color(); 126 | dataset.data = dataset.data.map(function(value) { 127 | return Utils.rand(0, 100); 128 | }); 129 | }); 130 | 131 | chart.update(); 132 | } 133 | }, 134 | { 135 | name: 'Add data', 136 | handler: function(chart) { 137 | chart.data.labels.push(chart.data.labels.length); 138 | chart.data.datasets.forEach(function (dataset, i) { 139 | dataset.data.push(Utils.rand(0, 100)); 140 | }); 141 | 142 | chart.update(); 143 | } 144 | }, 145 | { 146 | name: 'Remove data', 147 | handler: function(chart) { 148 | chart.data.labels.shift(); 149 | chart.data.datasets.forEach(function (dataset, i) { 150 | dataset.data.shift(); 151 | }); 152 | 153 | chart.update(); 154 | } 155 | } 156 | ] 157 | 158 | module.exports = { 159 | actions: actions, 160 | config: config, 161 | }; 162 | ``` 163 | --------------------------------------------------------------------------------